fusesell 1.2.8__py3-none-any.whl → 1.2.9__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.8.dist-info → fusesell-1.2.9.dist-info}/METADATA +1 -1
- {fusesell-1.2.8.dist-info → fusesell-1.2.9.dist-info}/RECORD +9 -9
- fusesell_local/__init__.py +1 -1
- fusesell_local/stages/initial_outreach.py +78 -4
- fusesell_local/utils/event_scheduler.py +42 -14
- {fusesell-1.2.8.dist-info → fusesell-1.2.9.dist-info}/WHEEL +0 -0
- {fusesell-1.2.8.dist-info → fusesell-1.2.9.dist-info}/entry_points.txt +0 -0
- {fusesell-1.2.8.dist-info → fusesell-1.2.9.dist-info}/licenses/LICENSE +0 -0
- {fusesell-1.2.8.dist-info → fusesell-1.2.9.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
fusesell.py,sha256=t5PjkhWEJGINp4k517u0EX0ge7lzuHOUHHro-BE1mGk,596
|
|
2
|
-
fusesell-1.2.
|
|
3
|
-
fusesell_local/__init__.py,sha256=
|
|
2
|
+
fusesell-1.2.9.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
|
|
3
|
+
fusesell_local/__init__.py,sha256=Ajbk9gus0-G2gR6WAo1F_QTApGpLzz01I2LL2iE6t8c,967
|
|
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=RMF_kgwNEc1ka8-CDJyzIOTSo8PGtR_zPKAgRevhlNo,39913
|
|
@@ -12,7 +12,7 @@ fusesell_local/stages/base_stage.py,sha256=ldo5xuHZto7ceEg3i_3rxAx0xPccK4n2jaxEJ
|
|
|
12
12
|
fusesell_local/stages/data_acquisition.py,sha256=Td3mwakJRoEYbi3od4v2ZzKOHLgLSgccZVxH3ezs1_4,71081
|
|
13
13
|
fusesell_local/stages/data_preparation.py,sha256=XWLg9b1w2NrMxLcrWDqB95mRmLQmVIMXpKNaBNr98TQ,52751
|
|
14
14
|
fusesell_local/stages/follow_up.py,sha256=H9Xek6EoIbHrerQvGTRswXDNFH6zq71DcRxxj0zpo2g,77747
|
|
15
|
-
fusesell_local/stages/initial_outreach.py,sha256=
|
|
15
|
+
fusesell_local/stages/initial_outreach.py,sha256=yoXAVaPgQXZc3bMq4U363Z4ARTsnSzOqbodKB3tke3A,123593
|
|
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
|
|
@@ -23,13 +23,13 @@ fusesell_local/tests/test_data_manager_teams.py,sha256=kjk4V4r9ja4EVREIiQMxkuZd4
|
|
|
23
23
|
fusesell_local/utils/__init__.py,sha256=TVemlo0wpckhNUxP3a1Tky3ekswy8JdIHaXBlkKXKBQ,330
|
|
24
24
|
fusesell_local/utils/birthday_email_manager.py,sha256=NKLoUyzPedyhewZPma21SOoU8p9wPquehloer7TRA9U,20478
|
|
25
25
|
fusesell_local/utils/data_manager.py,sha256=60CLOVkVB76AQx1wQyja0PFmA1t-YJITiGNni14IPOs,188449
|
|
26
|
-
fusesell_local/utils/event_scheduler.py,sha256=
|
|
26
|
+
fusesell_local/utils/event_scheduler.py,sha256=tP-rnx9Hixfcm6ZTqloLy_EgSPII89v5dSycRHrCTLE,39824
|
|
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.9.dist-info/METADATA,sha256=lsdyRarHbom144vFp0amieA9fQuDQF3-ZL9H-5A04DY,35074
|
|
32
|
+
fusesell-1.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
fusesell-1.2.9.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
|
|
34
|
+
fusesell-1.2.9.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
|
|
35
|
+
fusesell-1.2.9.dist-info/RECORD,,
|
fusesell_local/__init__.py
CHANGED
|
@@ -904,6 +904,7 @@ class InitialOutreachStage(BaseStage):
|
|
|
904
904
|
first_name = name_parts[0]
|
|
905
905
|
else:
|
|
906
906
|
first_name = contact_name or ''
|
|
907
|
+
context.setdefault('customer_first_name', first_name or contact_name or '')
|
|
907
908
|
|
|
908
909
|
action = input_data.get('action', 'draft_write')
|
|
909
910
|
action_labels = {
|
|
@@ -1044,7 +1045,7 @@ class InitialOutreachStage(BaseStage):
|
|
|
1044
1045
|
return rep
|
|
1045
1046
|
return reps[0] if reps else {}
|
|
1046
1047
|
|
|
1047
|
-
def _sanitize_email_body(self, html: str, staff_name: str, rep_profile: Dict[str, Any]) -> str:
|
|
1048
|
+
def _sanitize_email_body(self, html: str, staff_name: str, rep_profile: Dict[str, Any], customer_first_name: str) -> str:
|
|
1048
1049
|
if not html:
|
|
1049
1050
|
return ''
|
|
1050
1051
|
|
|
@@ -1065,12 +1066,84 @@ class InitialOutreachStage(BaseStage):
|
|
|
1065
1066
|
else:
|
|
1066
1067
|
html = html.replace(placeholder, '')
|
|
1067
1068
|
|
|
1068
|
-
# Remove any lingering placeholder fragments such as "[Your LinkedIn Profile"
|
|
1069
1069
|
html = re.sub(r'\[Your[^<\]]+\]?', '', html, flags=re.IGNORECASE)
|
|
1070
|
-
|
|
1070
|
+
|
|
1071
|
+
if '<p' not in html.lower():
|
|
1072
|
+
lines = [line.strip() for line in html.splitlines() if line.strip()]
|
|
1073
|
+
if lines:
|
|
1074
|
+
html = ''.join(f'<p>{line}</p>' for line in lines)
|
|
1075
|
+
|
|
1076
|
+
html = self._deduplicate_greeting(html, customer_first_name or '')
|
|
1071
1077
|
html = re.sub(r'(<p>\s*</p>)+', '', html, flags=re.IGNORECASE)
|
|
1072
1078
|
return html
|
|
1073
1079
|
|
|
1080
|
+
def _deduplicate_greeting(self, html: str, customer_first_name: str) -> str:
|
|
1081
|
+
paragraphs = re.findall(r'(<p.*?>.*?</p>)', html, flags=re.IGNORECASE | re.DOTALL)
|
|
1082
|
+
if not paragraphs:
|
|
1083
|
+
return html
|
|
1084
|
+
|
|
1085
|
+
greeting_seen = False
|
|
1086
|
+
cleaned: List[str] = []
|
|
1087
|
+
for para in paragraphs:
|
|
1088
|
+
text = self._strip_html_tags(para).strip()
|
|
1089
|
+
normalized_para = para
|
|
1090
|
+
if self._looks_like_greeting(text):
|
|
1091
|
+
normalized_para = self._standardize_greeting_paragraph(para, customer_first_name)
|
|
1092
|
+
if greeting_seen:
|
|
1093
|
+
continue
|
|
1094
|
+
greeting_seen = True
|
|
1095
|
+
cleaned.append(normalized_para)
|
|
1096
|
+
|
|
1097
|
+
remainder = re.sub(r'(<p.*?>.*?</p>)', '__PARA__', html, flags=re.IGNORECASE | re.DOTALL)
|
|
1098
|
+
rebuilt = ''
|
|
1099
|
+
idx = 0
|
|
1100
|
+
for segment in remainder.split('__PARA__'):
|
|
1101
|
+
rebuilt += segment
|
|
1102
|
+
if idx < len(cleaned):
|
|
1103
|
+
rebuilt += cleaned[idx]
|
|
1104
|
+
idx += 1
|
|
1105
|
+
if idx < len(cleaned):
|
|
1106
|
+
rebuilt += ''.join(cleaned[idx:])
|
|
1107
|
+
return rebuilt
|
|
1108
|
+
|
|
1109
|
+
def _looks_like_greeting(self, text: str) -> bool:
|
|
1110
|
+
lowered = text.lower().replace('\xa0', ' ').strip()
|
|
1111
|
+
return lowered.startswith(('hi ', 'hello ', 'dear '))
|
|
1112
|
+
|
|
1113
|
+
def _standardize_greeting_paragraph(self, paragraph_html: str, customer_first_name: str) -> str:
|
|
1114
|
+
text = self._strip_html_tags(paragraph_html).strip()
|
|
1115
|
+
lowered = text.lower()
|
|
1116
|
+
first_word = next((candidate.title() for candidate in ('dear', 'hello', 'hi') if lowered.startswith(candidate)), 'Hi')
|
|
1117
|
+
|
|
1118
|
+
if customer_first_name:
|
|
1119
|
+
greeting = f"{first_word} {customer_first_name},"
|
|
1120
|
+
else:
|
|
1121
|
+
greeting = f"{first_word} there,"
|
|
1122
|
+
|
|
1123
|
+
remainder = ''
|
|
1124
|
+
match = re.match(r' *(hi|hello|dear)\b[^,]*,(.*)', text, flags=re.IGNORECASE | re.DOTALL)
|
|
1125
|
+
if match:
|
|
1126
|
+
remainder = match.group(2).lstrip()
|
|
1127
|
+
elif lowered.startswith(('hi', 'hello', 'dear')):
|
|
1128
|
+
parts = text.split(',', 1)
|
|
1129
|
+
if len(parts) > 1:
|
|
1130
|
+
remainder = parts[1].lstrip()
|
|
1131
|
+
else:
|
|
1132
|
+
remainder = text[len(text.split(' ', 1)[0]):].lstrip()
|
|
1133
|
+
|
|
1134
|
+
if remainder:
|
|
1135
|
+
sanitized_text = f"{greeting} {remainder}".strip()
|
|
1136
|
+
else:
|
|
1137
|
+
sanitized_text = greeting
|
|
1138
|
+
|
|
1139
|
+
return re.sub(
|
|
1140
|
+
r'(<p.*?>).*?(</p>)',
|
|
1141
|
+
lambda m: f"{m.group(1)}{sanitized_text}{m.group(2)}",
|
|
1142
|
+
paragraph_html,
|
|
1143
|
+
count=1,
|
|
1144
|
+
flags=re.IGNORECASE | re.DOTALL
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1074
1147
|
def _extract_first_name(self, full_name: str) -> str:
|
|
1075
1148
|
if not full_name:
|
|
1076
1149
|
return ''
|
|
@@ -1177,7 +1250,8 @@ class InitialOutreachStage(BaseStage):
|
|
|
1177
1250
|
message_type = entry.get('message_type') or 'Email'
|
|
1178
1251
|
rep_profile = getattr(self, '_active_rep_profile', {}) or {}
|
|
1179
1252
|
staff_name = context.get('input_data', {}).get('staff_name') or self.config.get('staff_name', 'Sales Team')
|
|
1180
|
-
|
|
1253
|
+
first_name = context.get('customer_first_name') or context.get('input_data', {}).get('customer_name') or ''
|
|
1254
|
+
email_body = self._sanitize_email_body(email_body, staff_name, rep_profile, first_name)
|
|
1181
1255
|
|
|
1182
1256
|
metadata = {
|
|
1183
1257
|
'customer_company': customer_data.get('companyInfo', {}).get('name', 'Unknown'),
|
|
@@ -84,6 +84,7 @@ class EventScheduler:
|
|
|
84
84
|
status TEXT NOT NULL,
|
|
85
85
|
task TEXT NOT NULL,
|
|
86
86
|
cron TEXT NOT NULL,
|
|
87
|
+
cron_ts INTEGER,
|
|
87
88
|
room_id TEXT,
|
|
88
89
|
tags TEXT,
|
|
89
90
|
customextra TEXT,
|
|
@@ -115,6 +116,11 @@ class EventScheduler:
|
|
|
115
116
|
CREATE INDEX IF NOT EXISTS idx_reminder_task_cron
|
|
116
117
|
ON reminder_task(cron)
|
|
117
118
|
""")
|
|
119
|
+
|
|
120
|
+
cursor.execute("PRAGMA table_info(reminder_task)")
|
|
121
|
+
columns = {row[1] for row in cursor.fetchall()}
|
|
122
|
+
if 'cron_ts' not in columns:
|
|
123
|
+
cursor.execute("ALTER TABLE reminder_task ADD COLUMN cron_ts INTEGER")
|
|
118
124
|
|
|
119
125
|
conn.commit()
|
|
120
126
|
conn.close()
|
|
@@ -191,6 +197,19 @@ class EventScheduler:
|
|
|
191
197
|
except ValueError:
|
|
192
198
|
return value_str
|
|
193
199
|
|
|
200
|
+
def _to_unix_timestamp(self, value: Union[str, datetime, None]) -> Optional[int]:
|
|
201
|
+
"""
|
|
202
|
+
Convert a datetime-like value to a Unix timestamp (seconds).
|
|
203
|
+
"""
|
|
204
|
+
iso_value = self._format_datetime(value)
|
|
205
|
+
try:
|
|
206
|
+
parsed = datetime.fromisoformat(iso_value)
|
|
207
|
+
except ValueError:
|
|
208
|
+
return None
|
|
209
|
+
if parsed.tzinfo is None:
|
|
210
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
211
|
+
return int(parsed.timestamp())
|
|
212
|
+
|
|
194
213
|
def _build_reminder_payload(
|
|
195
214
|
self,
|
|
196
215
|
base_context: Dict[str, Any],
|
|
@@ -296,11 +315,13 @@ class EventScheduler:
|
|
|
296
315
|
|
|
297
316
|
cron_value = self._format_datetime(cron_value or send_time)
|
|
298
317
|
scheduled_time_str = self._format_datetime(scheduled_time_value or send_time)
|
|
318
|
+
cron_ts = self._to_unix_timestamp(cron_value)
|
|
299
319
|
|
|
300
320
|
return {
|
|
301
321
|
'status': status,
|
|
302
322
|
'task': task_label,
|
|
303
323
|
'cron': cron_value,
|
|
324
|
+
'cron_ts': cron_ts,
|
|
304
325
|
'room_id': room_id,
|
|
305
326
|
'tags': tags,
|
|
306
327
|
'customextra': customextra,
|
|
@@ -346,15 +367,20 @@ class EventScheduler:
|
|
|
346
367
|
conn = sqlite3.connect(self.main_db_path)
|
|
347
368
|
cursor = conn.cursor()
|
|
348
369
|
|
|
370
|
+
cron_ts = payload.get('cron_ts')
|
|
371
|
+
if cron_ts is None:
|
|
372
|
+
cron_ts = self._to_unix_timestamp(payload.get('cron'))
|
|
373
|
+
|
|
349
374
|
cursor.execute("""
|
|
350
375
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
376
|
+
(id, status, task, cron, cron_ts, room_id, tags, customextra, org_id, customer_id, task_id, import_uuid, scheduled_time)
|
|
377
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
353
378
|
""", (
|
|
354
379
|
reminder_id,
|
|
355
380
|
payload.get('status', 'published'),
|
|
356
381
|
payload.get('task') or 'FuseSell Reminder',
|
|
357
382
|
self._format_datetime(payload.get('cron')),
|
|
383
|
+
cron_ts,
|
|
358
384
|
payload.get('room_id'),
|
|
359
385
|
tags_str,
|
|
360
386
|
customextra_str,
|
|
@@ -455,6 +481,7 @@ class EventScheduler:
|
|
|
455
481
|
draft_id=draft_id,
|
|
456
482
|
customer_timezone=customer_timezone
|
|
457
483
|
)
|
|
484
|
+
reminder_payload.setdefault('cron_ts', self._to_unix_timestamp(reminder_payload.get('cron')))
|
|
458
485
|
reminder_task_id = self._insert_reminder_task(reminder_payload)
|
|
459
486
|
|
|
460
487
|
# Log the scheduling
|
|
@@ -585,6 +612,7 @@ class EventScheduler:
|
|
|
585
612
|
draft_id=original_draft_id,
|
|
586
613
|
customer_timezone=event_data['customer_timezone']
|
|
587
614
|
)
|
|
615
|
+
reminder_payload.setdefault('cron_ts', self._to_unix_timestamp(reminder_payload.get('cron')))
|
|
588
616
|
reminder_task_id = self._insert_reminder_task(reminder_payload)
|
|
589
617
|
|
|
590
618
|
self.logger.info(f"Scheduled follow-up event {followup_event_id} for {follow_up_time}")
|
|
@@ -834,18 +862,18 @@ class EventScheduler:
|
|
|
834
862
|
|
|
835
863
|
# Convert to list of dictionaries
|
|
836
864
|
events = []
|
|
837
|
-
for row in rows:
|
|
838
|
-
event = dict(zip(columns, row))
|
|
839
|
-
# Parse event_data JSON
|
|
840
|
-
if event['event_data']:
|
|
841
|
-
try:
|
|
842
|
-
event['event_data'] = json.loads(event['event_data'])
|
|
843
|
-
except json.JSONDecodeError:
|
|
844
|
-
pass
|
|
845
|
-
events.append(event)
|
|
846
|
-
|
|
847
|
-
return events
|
|
848
|
-
|
|
865
|
+
for row in rows:
|
|
866
|
+
event = dict(zip(columns, row))
|
|
867
|
+
# Parse event_data JSON
|
|
868
|
+
if event['event_data']:
|
|
869
|
+
try:
|
|
870
|
+
event['event_data'] = json.loads(event['event_data'])
|
|
871
|
+
except json.JSONDecodeError:
|
|
872
|
+
pass
|
|
873
|
+
events.append(event)
|
|
874
|
+
|
|
875
|
+
return events
|
|
876
|
+
|
|
849
877
|
except Exception as e:
|
|
850
878
|
self.logger.error(f"Failed to get scheduled events: {str(e)}")
|
|
851
879
|
return []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|