fusesell 1.2.9__py3-none-any.whl → 1.3.0__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.9.dist-info → fusesell-1.3.0.dist-info}/METADATA +1 -1
- {fusesell-1.2.9.dist-info → fusesell-1.3.0.dist-info}/RECORD +10 -10
- fusesell_local/__init__.py +1 -1
- fusesell_local/stages/initial_outreach.py +350 -184
- fusesell_local/utils/data_manager.py +8 -0
- fusesell_local/utils/event_scheduler.py +1 -1
- {fusesell-1.2.9.dist-info → fusesell-1.3.0.dist-info}/WHEEL +0 -0
- {fusesell-1.2.9.dist-info → fusesell-1.3.0.dist-info}/entry_points.txt +0 -0
- {fusesell-1.2.9.dist-info → fusesell-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {fusesell-1.2.9.dist-info → fusesell-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
fusesell.py,sha256=t5PjkhWEJGINp4k517u0EX0ge7lzuHOUHHro-BE1mGk,596
|
|
2
|
-
fusesell-1.
|
|
3
|
-
fusesell_local/__init__.py,sha256=
|
|
2
|
+
fusesell-1.3.0.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
|
|
3
|
+
fusesell_local/__init__.py,sha256=7shpYv9KVMmtCmwRUUrJOIid2cvvUuLpZwnbNNk4TS8,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=hvt0tpJQGDX5-k3pb2SR54IOnhQv2XyYXYtiVVECmts,131850
|
|
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=FHW9nvLXDgf-HYNFwxZlegZp0OgB3altszW6INIgyLM,188910
|
|
26
|
+
fusesell_local/utils/event_scheduler.py,sha256=TDk1v19cNgLhn2aJriQfpvZnwBcRpOWyHLDvkefW110,39834
|
|
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.
|
|
32
|
-
fusesell-1.
|
|
33
|
-
fusesell-1.
|
|
34
|
-
fusesell-1.
|
|
35
|
-
fusesell-1.
|
|
31
|
+
fusesell-1.3.0.dist-info/METADATA,sha256=cBJe2l9s_vsCApaTmAtH_tOjdq9berqTED9CGOtSoEM,35074
|
|
32
|
+
fusesell-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
fusesell-1.3.0.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
|
|
34
|
+
fusesell-1.3.0.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
|
|
35
|
+
fusesell-1.3.0.dist-info/RECORD,,
|
fusesell_local/__init__.py
CHANGED
|
@@ -7,7 +7,7 @@ import json
|
|
|
7
7
|
import uuid
|
|
8
8
|
import requests
|
|
9
9
|
import re
|
|
10
|
-
from typing import Dict, Any, List, Optional
|
|
10
|
+
from typing import Dict, Any, List, Optional
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
from .base_stage import BaseStage
|
|
13
13
|
|
|
@@ -169,11 +169,17 @@ class InitialOutreachStage(BaseStage):
|
|
|
169
169
|
raise ValueError(f"Draft not found: {selected_draft_id}")
|
|
170
170
|
|
|
171
171
|
# Get customer data for context
|
|
172
|
-
customer_data = self._get_customer_data(context)
|
|
173
|
-
scoring_data = self._get_scoring_data(context)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
customer_data = self._get_customer_data(context)
|
|
173
|
+
scoring_data = self._get_scoring_data(context)
|
|
174
|
+
|
|
175
|
+
recipient_identity = self._resolve_recipient_identity(customer_data, context)
|
|
176
|
+
context.setdefault('_recipient_identity', recipient_identity)
|
|
177
|
+
context.setdefault('_recipient_identity', recipient_identity)
|
|
178
|
+
if recipient_identity.get('first_name') and not context.get('customer_first_name'):
|
|
179
|
+
context['customer_first_name'] = recipient_identity['first_name']
|
|
180
|
+
|
|
181
|
+
# Rewrite the draft based on reason
|
|
182
|
+
rewritten_draft = self._rewrite_draft(existing_draft, reason, customer_data, scoring_data, context)
|
|
177
183
|
|
|
178
184
|
# Save the rewritten draft
|
|
179
185
|
saved_draft = self._save_rewritten_draft(context, rewritten_draft, selected_draft_id)
|
|
@@ -563,11 +569,11 @@ class InitialOutreachStage(BaseStage):
|
|
|
563
569
|
'lead_scoring': input_data.get('lead_scoring', [])
|
|
564
570
|
}
|
|
565
571
|
|
|
566
|
-
def _get_recommended_product(self, scoring_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
567
|
-
"""Get recommended product from scoring data."""
|
|
568
|
-
try:
|
|
569
|
-
# Try to get from analysis first
|
|
570
|
-
analysis = scoring_data.get('analysis', {})
|
|
572
|
+
def _get_recommended_product(self, scoring_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
573
|
+
"""Get recommended product from scoring data."""
|
|
574
|
+
try:
|
|
575
|
+
# Try to get from analysis first
|
|
576
|
+
analysis = scoring_data.get('analysis', {})
|
|
571
577
|
if 'recommended_product' in analysis:
|
|
572
578
|
return analysis['recommended_product']
|
|
573
579
|
|
|
@@ -584,14 +590,73 @@ class InitialOutreachStage(BaseStage):
|
|
|
584
590
|
|
|
585
591
|
return None
|
|
586
592
|
except Exception as e:
|
|
587
|
-
self.logger.error(f"Failed to get recommended product: {str(e)}")
|
|
588
|
-
return None
|
|
589
|
-
|
|
590
|
-
def
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
593
|
+
self.logger.error(f"Failed to get recommended product: {str(e)}")
|
|
594
|
+
return None
|
|
595
|
+
|
|
596
|
+
def _resolve_recipient_identity(
|
|
597
|
+
self,
|
|
598
|
+
customer_data: Dict[str, Any],
|
|
599
|
+
context: Dict[str, Any]
|
|
600
|
+
) -> Dict[str, Optional[str]]:
|
|
601
|
+
"""
|
|
602
|
+
Resolve recipient contact information and derive a safe first name.
|
|
603
|
+
"""
|
|
604
|
+
input_data = context.get('input_data', {}) or {}
|
|
605
|
+
stage_results = context.get('stage_results', {}) or {}
|
|
606
|
+
data_acquisition: Dict[str, Any] = {}
|
|
607
|
+
if isinstance(stage_results, dict):
|
|
608
|
+
data_acquisition = stage_results.get('data_acquisition', {}).get('data', {}) or {}
|
|
609
|
+
|
|
610
|
+
primary_contact = dict(customer_data.get('primaryContact', {}) or {})
|
|
611
|
+
|
|
612
|
+
recipient_email = (
|
|
613
|
+
input_data.get('recipient_address')
|
|
614
|
+
or primary_contact.get('email')
|
|
615
|
+
or primary_contact.get('emailAddress')
|
|
616
|
+
or data_acquisition.get('contact_email')
|
|
617
|
+
or data_acquisition.get('customer_email')
|
|
618
|
+
or input_data.get('customer_email')
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
recipient_name = (
|
|
622
|
+
input_data.get('recipient_name')
|
|
623
|
+
or primary_contact.get('name')
|
|
624
|
+
or primary_contact.get('fullName')
|
|
625
|
+
or data_acquisition.get('contact_name')
|
|
626
|
+
or data_acquisition.get('customer_contact')
|
|
627
|
+
or input_data.get('customer_name')
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
first_name_source = (
|
|
631
|
+
context.get('customer_first_name')
|
|
632
|
+
or input_data.get('customer_first_name')
|
|
633
|
+
or recipient_name
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
first_name = ''
|
|
637
|
+
if isinstance(first_name_source, str) and first_name_source.strip():
|
|
638
|
+
first_name = self._extract_first_name(first_name_source.strip())
|
|
639
|
+
if not first_name and isinstance(recipient_name, str) and recipient_name.strip():
|
|
640
|
+
first_name = self._extract_first_name(recipient_name.strip())
|
|
641
|
+
|
|
642
|
+
if recipient_name and not primary_contact.get('name'):
|
|
643
|
+
primary_contact['name'] = recipient_name
|
|
644
|
+
if recipient_email and not primary_contact.get('email'):
|
|
645
|
+
primary_contact['email'] = recipient_email
|
|
646
|
+
if primary_contact and isinstance(customer_data, dict):
|
|
647
|
+
customer_data['primaryContact'] = primary_contact
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
'email': recipient_email,
|
|
651
|
+
'full_name': recipient_name,
|
|
652
|
+
'first_name': first_name
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
def _get_auto_interaction_config(self, team_id: str = None) -> Dict[str, Any]:
|
|
656
|
+
"""
|
|
657
|
+
Get auto interaction configuration from team settings.
|
|
658
|
+
|
|
659
|
+
Args:
|
|
595
660
|
team_id: Team ID to get settings for
|
|
596
661
|
|
|
597
662
|
Returns:
|
|
@@ -648,6 +713,10 @@ class InitialOutreachStage(BaseStage):
|
|
|
648
713
|
try:
|
|
649
714
|
input_data = context.get('input_data', {})
|
|
650
715
|
rep_profile = rep_profile or {}
|
|
716
|
+
recipient_identity = self._resolve_recipient_identity(customer_data, context)
|
|
717
|
+
if recipient_identity.get('first_name') and not context.get('customer_first_name'):
|
|
718
|
+
context['customer_first_name'] = recipient_identity['first_name']
|
|
719
|
+
context.setdefault('_recipient_identity', recipient_identity)
|
|
651
720
|
if rep_profile:
|
|
652
721
|
primary_name = rep_profile.get('name')
|
|
653
722
|
if primary_name:
|
|
@@ -725,26 +794,33 @@ class InitialOutreachStage(BaseStage):
|
|
|
725
794
|
# Select the best subject line (first one, or most relevant)
|
|
726
795
|
selected_subject = subject_lines[0] if subject_lines else f"Partnership opportunity for {company_info.get('name', 'your company')}"
|
|
727
796
|
|
|
728
|
-
draft = {
|
|
729
|
-
'draft_id': draft_id,
|
|
730
|
-
'approach': approach['name'],
|
|
731
|
-
'tone': approach['tone'],
|
|
732
|
-
'focus': approach['focus'],
|
|
733
|
-
'subject': selected_subject, # Single subject instead of array
|
|
734
|
-
'subject_alternatives': subject_lines[1:4] if len(subject_lines) > 1 else [], # Store alternatives separately
|
|
735
|
-
'email_body': email_content,
|
|
736
|
-
'
|
|
737
|
-
'
|
|
738
|
-
'
|
|
739
|
-
'
|
|
740
|
-
'
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
'
|
|
746
|
-
|
|
747
|
-
|
|
797
|
+
draft = {
|
|
798
|
+
'draft_id': draft_id,
|
|
799
|
+
'approach': approach['name'],
|
|
800
|
+
'tone': approach['tone'],
|
|
801
|
+
'focus': approach['focus'],
|
|
802
|
+
'subject': selected_subject, # Single subject instead of array
|
|
803
|
+
'subject_alternatives': subject_lines[1:4] if len(subject_lines) > 1 else [], # Store alternatives separately
|
|
804
|
+
'email_body': email_content,
|
|
805
|
+
'email_format': 'html',
|
|
806
|
+
'recipient_email': recipient_identity.get('email'),
|
|
807
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
808
|
+
'customer_first_name': recipient_identity.get('first_name'),
|
|
809
|
+
'call_to_action': self._extract_call_to_action(email_content),
|
|
810
|
+
'personalization_score': self._calculate_personalization_score(email_content, customer_data),
|
|
811
|
+
'generated_at': datetime.now().isoformat(),
|
|
812
|
+
'status': 'draft',
|
|
813
|
+
'metadata': {
|
|
814
|
+
'customer_company': company_info.get('name', 'Unknown'),
|
|
815
|
+
'contact_name': contact_info.get('name', 'Unknown'),
|
|
816
|
+
'recipient_email': recipient_identity.get('email'),
|
|
817
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
818
|
+
'email_format': 'html',
|
|
819
|
+
'recommended_product': recommended_product.get('product_name', 'Unknown'),
|
|
820
|
+
'pain_points_addressed': len([p for p in pain_points if p.get('severity') in ['high', 'medium']]),
|
|
821
|
+
'generation_method': 'llm_powered'
|
|
822
|
+
}
|
|
823
|
+
}
|
|
748
824
|
|
|
749
825
|
generated_drafts.append(draft)
|
|
750
826
|
|
|
@@ -1193,18 +1269,22 @@ class InitialOutreachStage(BaseStage):
|
|
|
1193
1269
|
self.logger.debug('Skipping prompt entry because it is not a dict: %s', entry)
|
|
1194
1270
|
return None
|
|
1195
1271
|
|
|
1196
|
-
email_body = entry.get('body') or entry.get('content') or ''
|
|
1197
|
-
if isinstance(email_body, dict):
|
|
1198
|
-
email_body = email_body.get('html') or email_body.get('text') or email_body.get('content') or ''
|
|
1199
|
-
email_body = str(email_body).strip()
|
|
1200
|
-
if not email_body:
|
|
1201
|
-
self.logger.debug('Skipping prompt entry because email body is empty: %s', entry)
|
|
1202
|
-
return None
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
if
|
|
1206
|
-
|
|
1207
|
-
|
|
1272
|
+
email_body = entry.get('body') or entry.get('content') or ''
|
|
1273
|
+
if isinstance(email_body, dict):
|
|
1274
|
+
email_body = email_body.get('html') or email_body.get('text') or email_body.get('content') or ''
|
|
1275
|
+
email_body = str(email_body).strip()
|
|
1276
|
+
if not email_body:
|
|
1277
|
+
self.logger.debug('Skipping prompt entry because email body is empty: %s', entry)
|
|
1278
|
+
return None
|
|
1279
|
+
|
|
1280
|
+
recipient_identity = self._resolve_recipient_identity(customer_data, context)
|
|
1281
|
+
if recipient_identity.get('first_name') and not context.get('customer_first_name'):
|
|
1282
|
+
context['customer_first_name'] = recipient_identity['first_name']
|
|
1283
|
+
|
|
1284
|
+
subject = entry.get('subject')
|
|
1285
|
+
if isinstance(subject, list):
|
|
1286
|
+
subject = subject[0] if subject else ''
|
|
1287
|
+
subject = str(subject).strip() if subject else ''
|
|
1208
1288
|
|
|
1209
1289
|
subject_alternatives: List[str] = []
|
|
1210
1290
|
for key in ('subject_alternatives', 'subject_variations', 'subject_variants', 'alternative_subjects', 'subjects'):
|
|
@@ -1250,17 +1330,22 @@ class InitialOutreachStage(BaseStage):
|
|
|
1250
1330
|
message_type = entry.get('message_type') or 'Email'
|
|
1251
1331
|
rep_profile = getattr(self, '_active_rep_profile', {}) or {}
|
|
1252
1332
|
staff_name = context.get('input_data', {}).get('staff_name') or self.config.get('staff_name', 'Sales Team')
|
|
1253
|
-
first_name = context.get('customer_first_name') or context.get('input_data', {}).get('customer_name') or ''
|
|
1333
|
+
first_name = recipient_identity.get('first_name') or context.get('customer_first_name') or context.get('input_data', {}).get('customer_name') or ''
|
|
1254
1334
|
email_body = self._sanitize_email_body(email_body, staff_name, rep_profile, first_name)
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
'
|
|
1260
|
-
'
|
|
1261
|
-
'
|
|
1262
|
-
'
|
|
1263
|
-
|
|
1335
|
+
if '<html' not in email_body.lower():
|
|
1336
|
+
email_body = f"<html><body>{email_body}</body></html>"
|
|
1337
|
+
|
|
1338
|
+
metadata = {
|
|
1339
|
+
'customer_company': customer_data.get('companyInfo', {}).get('name', 'Unknown'),
|
|
1340
|
+
'contact_name': customer_data.get('primaryContact', {}).get('name', 'Unknown'),
|
|
1341
|
+
'recipient_email': recipient_identity.get('email'),
|
|
1342
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
1343
|
+
'email_format': 'html',
|
|
1344
|
+
'recommended_product': product_name or 'Unknown',
|
|
1345
|
+
'generation_method': 'prompt_template',
|
|
1346
|
+
'tags': tags,
|
|
1347
|
+
'message_type': message_type
|
|
1348
|
+
}
|
|
1264
1349
|
|
|
1265
1350
|
draft_id = entry.get('draft_id') or f"uuid:{str(uuid.uuid4())}"
|
|
1266
1351
|
draft_approach = "prompt"
|
|
@@ -1270,15 +1355,19 @@ class InitialOutreachStage(BaseStage):
|
|
|
1270
1355
|
'draft_id': draft_id,
|
|
1271
1356
|
'approach': approach,
|
|
1272
1357
|
'tone': mail_tone,
|
|
1273
|
-
'focus': focus,
|
|
1274
|
-
'subject': subject,
|
|
1275
|
-
'subject_alternatives': subject_alternatives,
|
|
1276
|
-
'email_body': email_body,
|
|
1277
|
-
'
|
|
1278
|
-
'
|
|
1279
|
-
'
|
|
1280
|
-
'
|
|
1281
|
-
'
|
|
1358
|
+
'focus': focus,
|
|
1359
|
+
'subject': subject,
|
|
1360
|
+
'subject_alternatives': subject_alternatives,
|
|
1361
|
+
'email_body': email_body,
|
|
1362
|
+
'email_format': 'html',
|
|
1363
|
+
'recipient_email': recipient_identity.get('email'),
|
|
1364
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
1365
|
+
'customer_first_name': recipient_identity.get('first_name'),
|
|
1366
|
+
'call_to_action': call_to_action,
|
|
1367
|
+
'product_mention': product_mention,
|
|
1368
|
+
'product_name': product_name,
|
|
1369
|
+
'priority_order': priority_order if priority_order is not None else 0,
|
|
1370
|
+
'personalization_score': personalization_score,
|
|
1282
1371
|
'generated_at': datetime.now().isoformat(),
|
|
1283
1372
|
'status': 'draft',
|
|
1284
1373
|
'metadata': metadata
|
|
@@ -1332,7 +1421,7 @@ class InitialOutreachStage(BaseStage):
|
|
|
1332
1421
|
)
|
|
1333
1422
|
|
|
1334
1423
|
# Clean and validate the generated content
|
|
1335
|
-
cleaned_content = self._clean_email_content(email_content)
|
|
1424
|
+
cleaned_content = self._clean_email_content(email_content, context)
|
|
1336
1425
|
|
|
1337
1426
|
return cleaned_content
|
|
1338
1427
|
|
|
@@ -1443,35 +1532,61 @@ Generate 4 subject lines, one per line, no numbering or bullets:"""
|
|
|
1443
1532
|
f"5-minute chat about {company_name}?"
|
|
1444
1533
|
]
|
|
1445
1534
|
|
|
1446
|
-
def _clean_email_content(
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
"
|
|
1458
|
-
"
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1535
|
+
def _clean_email_content(
|
|
1536
|
+
self,
|
|
1537
|
+
raw_content: str,
|
|
1538
|
+
context: Optional[Dict[str, Any]] = None
|
|
1539
|
+
) -> str:
|
|
1540
|
+
"""
|
|
1541
|
+
Clean and normalize generated email content, returning HTML.
|
|
1542
|
+
"""
|
|
1543
|
+
content = (raw_content or "").replace("\r\n", "\n").strip()
|
|
1544
|
+
|
|
1545
|
+
artifacts_to_remove = (
|
|
1546
|
+
"Here's the email:",
|
|
1547
|
+
"Here is the email:",
|
|
1548
|
+
"Email content:",
|
|
1549
|
+
"Generated email:",
|
|
1550
|
+
"Subject:",
|
|
1551
|
+
"Email:"
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
for artifact in artifacts_to_remove:
|
|
1555
|
+
if content.startswith(artifact):
|
|
1556
|
+
content = content[len(artifact):].strip()
|
|
1557
|
+
|
|
1558
|
+
lines = [line.strip() for line in content.split('\n') if line.strip()]
|
|
1559
|
+
content = '\n\n'.join(lines)
|
|
1560
|
+
|
|
1561
|
+
if not content:
|
|
1562
|
+
return "<html><body></body></html>"
|
|
1563
|
+
|
|
1564
|
+
if not content.lower().startswith(('dear ', 'hi ', 'hello ', 'greetings')):
|
|
1565
|
+
content = f"Dear Valued Customer,\n\n{content}"
|
|
1566
|
+
|
|
1567
|
+
closings = ('best regards', 'sincerely', 'thanks', 'thank you', 'kind regards')
|
|
1568
|
+
if not any(closing in content.lower() for closing in closings):
|
|
1569
|
+
content += "\n\nBest regards,\n[Your Name]"
|
|
1570
|
+
|
|
1571
|
+
ctx = context or {}
|
|
1572
|
+
input_data = ctx.get('input_data', {}) or {}
|
|
1573
|
+
rep_profile = getattr(self, '_active_rep_profile', {}) or {}
|
|
1574
|
+
staff_name = input_data.get('staff_name') or self.config.get('staff_name') or 'Sales Team'
|
|
1575
|
+
identity = ctx.get('_recipient_identity') or {}
|
|
1576
|
+
customer_first_name = (
|
|
1577
|
+
identity.get('first_name')
|
|
1578
|
+
or ctx.get('customer_first_name')
|
|
1579
|
+
or input_data.get('customer_first_name')
|
|
1580
|
+
or input_data.get('recipient_name')
|
|
1581
|
+
or input_data.get('customer_name')
|
|
1582
|
+
or ''
|
|
1583
|
+
)
|
|
1584
|
+
|
|
1585
|
+
sanitized = self._sanitize_email_body(content, staff_name, rep_profile, customer_first_name)
|
|
1586
|
+
if '<html' not in sanitized.lower():
|
|
1587
|
+
sanitized = f"<html><body>{sanitized}</body></html>"
|
|
1588
|
+
|
|
1589
|
+
return sanitized
|
|
1475
1590
|
|
|
1476
1591
|
def _extract_call_to_action(self, email_content: str) -> str:
|
|
1477
1592
|
"""Extract the main call-to-action from email content."""
|
|
@@ -1563,55 +1678,85 @@ Best regards,
|
|
|
1563
1678
|
|
|
1564
1679
|
def _generate_fallback_draft(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
1565
1680
|
"""Generate fallback draft when LLM generation fails."""
|
|
1566
|
-
draft_id = f"uuid:{str(uuid.uuid4())}"
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
'
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1681
|
+
draft_id = f"uuid:{str(uuid.uuid4())}"
|
|
1682
|
+
recipient_identity = self._resolve_recipient_identity(customer_data, context)
|
|
1683
|
+
context.setdefault('_recipient_identity', recipient_identity)
|
|
1684
|
+
if recipient_identity.get('first_name') and not context.get('customer_first_name'):
|
|
1685
|
+
context['customer_first_name'] = recipient_identity['first_name']
|
|
1686
|
+
|
|
1687
|
+
template_body = self._generate_template_email(
|
|
1688
|
+
customer_data,
|
|
1689
|
+
recommended_product,
|
|
1690
|
+
{'tone': 'professional'},
|
|
1691
|
+
context
|
|
1692
|
+
)
|
|
1693
|
+
email_body = self._clean_email_content(template_body, context)
|
|
1694
|
+
|
|
1695
|
+
fallback_subjects = self._generate_fallback_subject_lines(customer_data, recommended_product)
|
|
1696
|
+
|
|
1697
|
+
return [{
|
|
1698
|
+
'draft_id': draft_id,
|
|
1699
|
+
'approach': 'fallback_template',
|
|
1700
|
+
'tone': 'professional',
|
|
1701
|
+
'focus': 'general outreach',
|
|
1702
|
+
'subject': fallback_subjects[0],
|
|
1703
|
+
'subject_alternatives': fallback_subjects[1:],
|
|
1704
|
+
'email_body': email_body,
|
|
1705
|
+
'email_format': 'html',
|
|
1706
|
+
'recipient_email': recipient_identity.get('email'),
|
|
1707
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
1708
|
+
'customer_first_name': recipient_identity.get('first_name'),
|
|
1709
|
+
'call_to_action': 'Would you be interested in a brief call?',
|
|
1710
|
+
'personalization_score': 50,
|
|
1711
|
+
'generated_at': datetime.now().isoformat(),
|
|
1712
|
+
'status': 'draft',
|
|
1713
|
+
'metadata': {
|
|
1714
|
+
'generation_method': 'template_fallback',
|
|
1715
|
+
'note': 'Generated using template due to LLM failure',
|
|
1716
|
+
'recipient_email': recipient_identity.get('email'),
|
|
1717
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
1718
|
+
'email_format': 'html'
|
|
1719
|
+
}
|
|
1720
|
+
}]
|
|
1587
1721
|
|
|
1588
1722
|
def _get_mock_email_drafts(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
1589
1723
|
"""Get mock email drafts for dry run."""
|
|
1590
1724
|
input_data = context.get('input_data', {})
|
|
1591
1725
|
company_info = customer_data.get('companyInfo', {})
|
|
1592
1726
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
'
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
'
|
|
1610
|
-
'
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1727
|
+
recipient_identity = self._resolve_recipient_identity(customer_data, context)
|
|
1728
|
+
context.setdefault('_recipient_identity', recipient_identity)
|
|
1729
|
+
mock_body = f"""[DRY RUN] Mock email content for {company_info.get('name', 'Test Company')}
|
|
1730
|
+
|
|
1731
|
+
This is a mock email that would be generated for testing purposes. In a real execution, this would contain personalized content based on the customer's company information, pain points, and our product recommendations."""
|
|
1732
|
+
|
|
1733
|
+
return [{
|
|
1734
|
+
'draft_id': 'mock_draft_001',
|
|
1735
|
+
'approach': 'professional_direct',
|
|
1736
|
+
'tone': 'professional and direct',
|
|
1737
|
+
'focus': 'business value and ROI',
|
|
1738
|
+
'subject': f"Partnership Opportunity for {company_info.get('name', 'Test Company')}",
|
|
1739
|
+
'subject_alternatives': [
|
|
1740
|
+
f"Quick Question About {company_info.get('name', 'Test Company')}",
|
|
1741
|
+
f"Helping Companies Like {company_info.get('name', 'Test Company')}"
|
|
1742
|
+
],
|
|
1743
|
+
'email_body': self._clean_email_content(mock_body, context),
|
|
1744
|
+
'email_format': 'html',
|
|
1745
|
+
'recipient_email': recipient_identity.get('email'),
|
|
1746
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
1747
|
+
'customer_first_name': recipient_identity.get('first_name'),
|
|
1748
|
+
'call_to_action': 'Mock call to action',
|
|
1749
|
+
'personalization_score': 85,
|
|
1750
|
+
'generated_at': datetime.now().isoformat(),
|
|
1751
|
+
'status': 'mock',
|
|
1752
|
+
'metadata': {
|
|
1753
|
+
'generation_method': 'mock_data',
|
|
1754
|
+
'note': 'This is mock data for dry run testing',
|
|
1755
|
+
'recipient_email': recipient_identity.get('email'),
|
|
1756
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
1757
|
+
'email_format': 'html'
|
|
1758
|
+
}
|
|
1759
|
+
}]
|
|
1615
1760
|
|
|
1616
1761
|
|
|
1617
1762
|
def _convert_draft_to_server_format(self, draft: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -1950,16 +2095,17 @@ This is a mock email that would be generated for testing purposes. In a real exe
|
|
|
1950
2095
|
|
|
1951
2096
|
def _rewrite_draft(self, existing_draft: Dict[str, Any], reason: str, customer_data: Dict[str, Any], scoring_data: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
1952
2097
|
"""Rewrite existing draft based on reason using LLM."""
|
|
1953
|
-
try:
|
|
1954
|
-
if self.is_dry_run():
|
|
1955
|
-
rewritten = existing_draft.copy()
|
|
1956
|
-
rewritten['draft_id'] = f"uuid:{str(uuid.uuid4())}"
|
|
1957
|
-
rewritten['draft_approach'] = "rewrite"
|
|
1958
|
-
rewritten['draft_type'] = "rewrite"
|
|
1959
|
-
rewritten['email_body'] = f"[DRY RUN - REWRITTEN: {reason}] " + existing_draft.get('email_body', '')
|
|
1960
|
-
rewritten['rewrite_reason'] = reason
|
|
1961
|
-
rewritten['rewritten_at'] = datetime.now().isoformat()
|
|
1962
|
-
|
|
2098
|
+
try:
|
|
2099
|
+
if self.is_dry_run():
|
|
2100
|
+
rewritten = existing_draft.copy()
|
|
2101
|
+
rewritten['draft_id'] = f"uuid:{str(uuid.uuid4())}"
|
|
2102
|
+
rewritten['draft_approach'] = "rewrite"
|
|
2103
|
+
rewritten['draft_type'] = "rewrite"
|
|
2104
|
+
rewritten['email_body'] = f"[DRY RUN - REWRITTEN: {reason}] " + existing_draft.get('email_body', '')
|
|
2105
|
+
rewritten['rewrite_reason'] = reason
|
|
2106
|
+
rewritten['rewritten_at'] = datetime.now().isoformat()
|
|
2107
|
+
rewritten.setdefault('email_format', 'html')
|
|
2108
|
+
return rewritten
|
|
1963
2109
|
|
|
1964
2110
|
input_data = context.get('input_data', {})
|
|
1965
2111
|
company_info = customer_data.get('companyInfo', {})
|
|
@@ -1997,46 +2143,66 @@ Generate only the rewritten email content:"""
|
|
|
1997
2143
|
)
|
|
1998
2144
|
|
|
1999
2145
|
# Clean the rewritten content
|
|
2000
|
-
cleaned_content = self._clean_email_content(rewritten_content)
|
|
2001
|
-
|
|
2002
|
-
# Create rewritten draft object
|
|
2003
|
-
rewritten = existing_draft.copy()
|
|
2004
|
-
rewritten['draft_id'] = f"uuid:{str(uuid.uuid4())}"
|
|
2005
|
-
rewritten['draft_approach'] = "rewrite"
|
|
2006
|
-
rewritten['draft_type'] = "rewrite"
|
|
2007
|
-
rewritten['email_body'] = cleaned_content
|
|
2008
|
-
rewritten['rewrite_reason'] = reason
|
|
2009
|
-
rewritten['rewritten_at'] = datetime.now().isoformat()
|
|
2010
|
-
rewritten['version'] = existing_draft.get('version', 1) + 1
|
|
2011
|
-
rewritten['call_to_action'] = self._extract_call_to_action(cleaned_content)
|
|
2012
|
-
rewritten['personalization_score'] = self._calculate_personalization_score(cleaned_content, customer_data)
|
|
2146
|
+
cleaned_content = self._clean_email_content(rewritten_content, context)
|
|
2147
|
+
|
|
2148
|
+
# Create rewritten draft object
|
|
2149
|
+
rewritten = existing_draft.copy()
|
|
2150
|
+
rewritten['draft_id'] = f"uuid:{str(uuid.uuid4())}"
|
|
2151
|
+
rewritten['draft_approach'] = "rewrite"
|
|
2152
|
+
rewritten['draft_type'] = "rewrite"
|
|
2153
|
+
rewritten['email_body'] = cleaned_content
|
|
2154
|
+
rewritten['rewrite_reason'] = reason
|
|
2155
|
+
rewritten['rewritten_at'] = datetime.now().isoformat()
|
|
2156
|
+
rewritten['version'] = existing_draft.get('version', 1) + 1
|
|
2157
|
+
rewritten['call_to_action'] = self._extract_call_to_action(cleaned_content)
|
|
2158
|
+
rewritten['personalization_score'] = self._calculate_personalization_score(cleaned_content, customer_data)
|
|
2159
|
+
recipient_identity = context.get('_recipient_identity') or self._resolve_recipient_identity(customer_data, context)
|
|
2160
|
+
if recipient_identity:
|
|
2161
|
+
rewritten['recipient_email'] = recipient_identity.get('email')
|
|
2162
|
+
rewritten['recipient_name'] = recipient_identity.get('full_name')
|
|
2163
|
+
rewritten['customer_first_name'] = recipient_identity.get('first_name')
|
|
2164
|
+
rewritten['email_format'] = 'html'
|
|
2013
2165
|
|
|
2014
2166
|
# Update metadata
|
|
2015
2167
|
if 'metadata' not in rewritten:
|
|
2016
2168
|
rewritten['metadata'] = {}
|
|
2017
2169
|
rewritten['metadata']['rewrite_history'] = rewritten['metadata'].get('rewrite_history', [])
|
|
2018
|
-
rewritten['metadata']['rewrite_history'].append({
|
|
2019
|
-
'reason': reason,
|
|
2020
|
-
'rewritten_at': datetime.now().isoformat(),
|
|
2021
|
-
'original_draft_id': existing_draft.get('draft_id')
|
|
2022
|
-
})
|
|
2023
|
-
rewritten['metadata']['generation_method'] = 'llm_rewrite'
|
|
2170
|
+
rewritten['metadata']['rewrite_history'].append({
|
|
2171
|
+
'reason': reason,
|
|
2172
|
+
'rewritten_at': datetime.now().isoformat(),
|
|
2173
|
+
'original_draft_id': existing_draft.get('draft_id')
|
|
2174
|
+
})
|
|
2175
|
+
rewritten['metadata']['generation_method'] = 'llm_rewrite'
|
|
2176
|
+
if recipient_identity:
|
|
2177
|
+
rewritten['metadata']['recipient_email'] = recipient_identity.get('email')
|
|
2178
|
+
rewritten['metadata']['recipient_name'] = recipient_identity.get('full_name')
|
|
2179
|
+
rewritten['metadata']['email_format'] = 'html'
|
|
2024
2180
|
|
|
2025
2181
|
self.logger.info(f"Successfully rewrote draft based on reason: {reason}")
|
|
2026
2182
|
return rewritten
|
|
2027
2183
|
|
|
2028
|
-
except Exception as e:
|
|
2029
|
-
self.logger.error(f"Failed to rewrite draft: {str(e)}")
|
|
2030
|
-
# Fallback to simple modification
|
|
2031
|
-
rewritten = existing_draft.copy()
|
|
2032
|
-
rewritten['draft_id'] = f"uuid:{str(uuid.uuid4())}"
|
|
2033
|
-
rewritten['draft_approach'] = "rewrite"
|
|
2034
|
-
rewritten['draft_type'] = "rewrite"
|
|
2035
|
-
rewritten['email_body'] = f"[REWRITTEN: {reason}]\n\n" + existing_draft.get('email_body', '')
|
|
2036
|
-
rewritten['rewrite_reason'] = reason
|
|
2037
|
-
rewritten['rewritten_at'] = datetime.now().isoformat()
|
|
2038
|
-
rewritten['metadata'] = {'generation_method': 'template_rewrite', 'error': str(e)}
|
|
2039
|
-
|
|
2184
|
+
except Exception as e:
|
|
2185
|
+
self.logger.error(f"Failed to rewrite draft: {str(e)}")
|
|
2186
|
+
# Fallback to simple modification
|
|
2187
|
+
rewritten = existing_draft.copy()
|
|
2188
|
+
rewritten['draft_id'] = f"uuid:{str(uuid.uuid4())}"
|
|
2189
|
+
rewritten['draft_approach'] = "rewrite"
|
|
2190
|
+
rewritten['draft_type'] = "rewrite"
|
|
2191
|
+
rewritten['email_body'] = f"[REWRITTEN: {reason}]\n\n" + existing_draft.get('email_body', '')
|
|
2192
|
+
rewritten['rewrite_reason'] = reason
|
|
2193
|
+
rewritten['rewritten_at'] = datetime.now().isoformat()
|
|
2194
|
+
rewritten['metadata'] = {'generation_method': 'template_rewrite', 'error': str(e)}
|
|
2195
|
+
rewritten['email_body'] = self._clean_email_content(rewritten['email_body'], context)
|
|
2196
|
+
fallback_identity = context.get('_recipient_identity') or self._resolve_recipient_identity(customer_data, context)
|
|
2197
|
+
if fallback_identity:
|
|
2198
|
+
rewritten['recipient_email'] = fallback_identity.get('email')
|
|
2199
|
+
rewritten['recipient_name'] = fallback_identity.get('full_name')
|
|
2200
|
+
rewritten['customer_first_name'] = fallback_identity.get('first_name')
|
|
2201
|
+
rewritten['metadata']['recipient_email'] = fallback_identity.get('email')
|
|
2202
|
+
rewritten['metadata']['recipient_name'] = fallback_identity.get('full_name')
|
|
2203
|
+
rewritten['metadata']['email_format'] = 'html'
|
|
2204
|
+
rewritten['email_format'] = 'html'
|
|
2205
|
+
return rewritten
|
|
2040
2206
|
|
|
2041
2207
|
def _save_rewritten_draft(self, context: Dict[str, Any], rewritten_draft: Dict[str, Any], original_draft_id: str) -> Dict[str, Any]:
|
|
2042
2208
|
"""Save rewritten draft to database and file system."""
|
|
@@ -552,6 +552,7 @@ class LocalDataManager:
|
|
|
552
552
|
status TEXT NOT NULL,
|
|
553
553
|
task TEXT NOT NULL,
|
|
554
554
|
cron TEXT NOT NULL,
|
|
555
|
+
cron_ts INTEGER,
|
|
555
556
|
room_id TEXT,
|
|
556
557
|
tags TEXT,
|
|
557
558
|
customextra TEXT,
|
|
@@ -567,6 +568,11 @@ class LocalDataManager:
|
|
|
567
568
|
)
|
|
568
569
|
""")
|
|
569
570
|
|
|
571
|
+
cursor.execute("PRAGMA table_info(reminder_task)")
|
|
572
|
+
reminder_columns = {row[1] for row in cursor.fetchall()}
|
|
573
|
+
if 'cron_ts' not in reminder_columns:
|
|
574
|
+
cursor.execute("ALTER TABLE reminder_task ADD COLUMN cron_ts INTEGER")
|
|
575
|
+
|
|
570
576
|
# Create extracted_files table (equivalent to gs_plan_setting_extracted_file)
|
|
571
577
|
cursor.execute("""
|
|
572
578
|
CREATE TABLE IF NOT EXISTS extracted_files (
|
|
@@ -684,6 +690,8 @@ class LocalDataManager:
|
|
|
684
690
|
"CREATE INDEX IF NOT EXISTS idx_reminder_task_task_id ON reminder_task(task_id)")
|
|
685
691
|
cursor.execute(
|
|
686
692
|
"CREATE INDEX IF NOT EXISTS idx_reminder_task_cron ON reminder_task(cron)")
|
|
693
|
+
cursor.execute(
|
|
694
|
+
"CREATE INDEX IF NOT EXISTS idx_reminder_task_cron_ts ON reminder_task(cron_ts)")
|
|
687
695
|
cursor.execute(
|
|
688
696
|
"CREATE INDEX IF NOT EXISTS idx_extracted_files_org_id ON extracted_files(org_id)")
|
|
689
697
|
cursor.execute(
|
|
@@ -4,7 +4,7 @@ Creates scheduled events in database for external app to handle
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
|
-
from datetime import datetime, timedelta
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
8
|
from typing import Dict, Any, Optional, List, Union
|
|
9
9
|
import pytz
|
|
10
10
|
import json
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|