fusesell 1.2.9__py3-none-any.whl → 1.3.1__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.1.dist-info}/METADATA +1 -1
- {fusesell-1.2.9.dist-info → fusesell-1.3.1.dist-info}/RECORD +10 -10
- fusesell_local/__init__.py +1 -1
- fusesell_local/stages/initial_outreach.py +438 -208
- 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.1.dist-info}/WHEEL +0 -0
- {fusesell-1.2.9.dist-info → fusesell-1.3.1.dist-info}/entry_points.txt +0 -0
- {fusesell-1.2.9.dist-info → fusesell-1.3.1.dist-info}/licenses/LICENSE +0 -0
- {fusesell-1.2.9.dist-info → fusesell-1.3.1.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.1.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
|
|
3
|
+
fusesell_local/__init__.py,sha256=4eRHhad_hjqLaoWSd_wZzPRBuX_V_Aqqf2_o93NeHao,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=8Ra-Nmq4njAG1iMNiUW7FQbYnX0h9p5F59OXlyGdFdU,135285
|
|
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.1.dist-info/METADATA,sha256=hYcoE3sJK-2TPuxHSUv5yxaoNYu1_B5gW1bfScica5w,35074
|
|
32
|
+
fusesell-1.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
fusesell-1.3.1.dist-info/entry_points.txt,sha256=Vqek7tbiX7iF4rQkCRBZvT5WrB0HUduqKTsI2ZjhsXo,53
|
|
34
|
+
fusesell-1.3.1.dist-info/top_level.txt,sha256=VP9y1K6DEq6gNq2UgLd7ChujxViF6OzeAVCK7IUBXPA,24
|
|
35
|
+
fusesell-1.3.1.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:
|
|
@@ -707,12 +776,13 @@ class InitialOutreachStage(BaseStage):
|
|
|
707
776
|
|
|
708
777
|
for approach in draft_approaches:
|
|
709
778
|
try:
|
|
710
|
-
# Generate email content for this approach
|
|
711
|
-
email_content = self._generate_single_email_draft(
|
|
712
|
-
customer_data, recommended_product, scoring_data,
|
|
713
|
-
approach, context
|
|
714
|
-
)
|
|
715
|
-
|
|
779
|
+
# Generate email content for this approach
|
|
780
|
+
email_content = self._generate_single_email_draft(
|
|
781
|
+
customer_data, recommended_product, scoring_data,
|
|
782
|
+
approach, context
|
|
783
|
+
)
|
|
784
|
+
email_content = self._ensure_html_email(email_content, context)
|
|
785
|
+
|
|
716
786
|
# Generate subject lines for this approach
|
|
717
787
|
subject_lines = self._generate_subject_lines(
|
|
718
788
|
customer_data, recommended_product, approach, context
|
|
@@ -725,26 +795,33 @@ class InitialOutreachStage(BaseStage):
|
|
|
725
795
|
# Select the best subject line (first one, or most relevant)
|
|
726
796
|
selected_subject = subject_lines[0] if subject_lines else f"Partnership opportunity for {company_info.get('name', 'your company')}"
|
|
727
797
|
|
|
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
|
-
|
|
798
|
+
draft = {
|
|
799
|
+
'draft_id': draft_id,
|
|
800
|
+
'approach': approach['name'],
|
|
801
|
+
'tone': approach['tone'],
|
|
802
|
+
'focus': approach['focus'],
|
|
803
|
+
'subject': selected_subject, # Single subject instead of array
|
|
804
|
+
'subject_alternatives': subject_lines[1:4] if len(subject_lines) > 1 else [], # Store alternatives separately
|
|
805
|
+
'email_body': email_content,
|
|
806
|
+
'email_format': 'html',
|
|
807
|
+
'recipient_email': recipient_identity.get('email'),
|
|
808
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
809
|
+
'customer_first_name': recipient_identity.get('first_name'),
|
|
810
|
+
'call_to_action': self._extract_call_to_action(email_content),
|
|
811
|
+
'personalization_score': self._calculate_personalization_score(email_content, customer_data),
|
|
812
|
+
'generated_at': datetime.now().isoformat(),
|
|
813
|
+
'status': 'draft',
|
|
814
|
+
'metadata': {
|
|
815
|
+
'customer_company': company_info.get('name', 'Unknown'),
|
|
816
|
+
'contact_name': contact_info.get('name', 'Unknown'),
|
|
817
|
+
'recipient_email': recipient_identity.get('email'),
|
|
818
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
819
|
+
'email_format': 'html',
|
|
820
|
+
'recommended_product': recommended_product.get('product_name', 'Unknown'),
|
|
821
|
+
'pain_points_addressed': len([p for p in pain_points if p.get('severity') in ['high', 'medium']]),
|
|
822
|
+
'generation_method': 'llm_powered'
|
|
823
|
+
}
|
|
824
|
+
}
|
|
748
825
|
|
|
749
826
|
generated_drafts.append(draft)
|
|
750
827
|
|
|
@@ -1193,18 +1270,22 @@ class InitialOutreachStage(BaseStage):
|
|
|
1193
1270
|
self.logger.debug('Skipping prompt entry because it is not a dict: %s', entry)
|
|
1194
1271
|
return None
|
|
1195
1272
|
|
|
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
|
-
|
|
1273
|
+
email_body = entry.get('body') or entry.get('content') or ''
|
|
1274
|
+
if isinstance(email_body, dict):
|
|
1275
|
+
email_body = email_body.get('html') or email_body.get('text') or email_body.get('content') or ''
|
|
1276
|
+
email_body = str(email_body).strip()
|
|
1277
|
+
if not email_body:
|
|
1278
|
+
self.logger.debug('Skipping prompt entry because email body is empty: %s', entry)
|
|
1279
|
+
return None
|
|
1280
|
+
|
|
1281
|
+
recipient_identity = self._resolve_recipient_identity(customer_data, context)
|
|
1282
|
+
if recipient_identity.get('first_name') and not context.get('customer_first_name'):
|
|
1283
|
+
context['customer_first_name'] = recipient_identity['first_name']
|
|
1284
|
+
|
|
1285
|
+
subject = entry.get('subject')
|
|
1286
|
+
if isinstance(subject, list):
|
|
1287
|
+
subject = subject[0] if subject else ''
|
|
1288
|
+
subject = str(subject).strip() if subject else ''
|
|
1208
1289
|
|
|
1209
1290
|
subject_alternatives: List[str] = []
|
|
1210
1291
|
for key in ('subject_alternatives', 'subject_variations', 'subject_variants', 'alternative_subjects', 'subjects'):
|
|
@@ -1250,17 +1331,22 @@ class InitialOutreachStage(BaseStage):
|
|
|
1250
1331
|
message_type = entry.get('message_type') or 'Email'
|
|
1251
1332
|
rep_profile = getattr(self, '_active_rep_profile', {}) or {}
|
|
1252
1333
|
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 ''
|
|
1334
|
+
first_name = recipient_identity.get('first_name') or context.get('customer_first_name') or context.get('input_data', {}).get('customer_name') or ''
|
|
1254
1335
|
email_body = self._sanitize_email_body(email_body, staff_name, rep_profile, first_name)
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
'
|
|
1260
|
-
'
|
|
1261
|
-
'
|
|
1262
|
-
'
|
|
1263
|
-
|
|
1336
|
+
if '<html' not in email_body.lower():
|
|
1337
|
+
email_body = f"<html><body>{email_body}</body></html>"
|
|
1338
|
+
|
|
1339
|
+
metadata = {
|
|
1340
|
+
'customer_company': customer_data.get('companyInfo', {}).get('name', 'Unknown'),
|
|
1341
|
+
'contact_name': customer_data.get('primaryContact', {}).get('name', 'Unknown'),
|
|
1342
|
+
'recipient_email': recipient_identity.get('email'),
|
|
1343
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
1344
|
+
'email_format': 'html',
|
|
1345
|
+
'recommended_product': product_name or 'Unknown',
|
|
1346
|
+
'generation_method': 'prompt_template',
|
|
1347
|
+
'tags': tags,
|
|
1348
|
+
'message_type': message_type
|
|
1349
|
+
}
|
|
1264
1350
|
|
|
1265
1351
|
draft_id = entry.get('draft_id') or f"uuid:{str(uuid.uuid4())}"
|
|
1266
1352
|
draft_approach = "prompt"
|
|
@@ -1270,15 +1356,19 @@ class InitialOutreachStage(BaseStage):
|
|
|
1270
1356
|
'draft_id': draft_id,
|
|
1271
1357
|
'approach': approach,
|
|
1272
1358
|
'tone': mail_tone,
|
|
1273
|
-
'focus': focus,
|
|
1274
|
-
'subject': subject,
|
|
1275
|
-
'subject_alternatives': subject_alternatives,
|
|
1276
|
-
'email_body': email_body,
|
|
1277
|
-
'
|
|
1278
|
-
'
|
|
1279
|
-
'
|
|
1280
|
-
'
|
|
1281
|
-
'
|
|
1359
|
+
'focus': focus,
|
|
1360
|
+
'subject': subject,
|
|
1361
|
+
'subject_alternatives': subject_alternatives,
|
|
1362
|
+
'email_body': email_body,
|
|
1363
|
+
'email_format': 'html',
|
|
1364
|
+
'recipient_email': recipient_identity.get('email'),
|
|
1365
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
1366
|
+
'customer_first_name': recipient_identity.get('first_name'),
|
|
1367
|
+
'call_to_action': call_to_action,
|
|
1368
|
+
'product_mention': product_mention,
|
|
1369
|
+
'product_name': product_name,
|
|
1370
|
+
'priority_order': priority_order if priority_order is not None else 0,
|
|
1371
|
+
'personalization_score': personalization_score,
|
|
1282
1372
|
'generated_at': datetime.now().isoformat(),
|
|
1283
1373
|
'status': 'draft',
|
|
1284
1374
|
'metadata': metadata
|
|
@@ -1332,7 +1422,7 @@ class InitialOutreachStage(BaseStage):
|
|
|
1332
1422
|
)
|
|
1333
1423
|
|
|
1334
1424
|
# Clean and validate the generated content
|
|
1335
|
-
cleaned_content = self._clean_email_content(email_content)
|
|
1425
|
+
cleaned_content = self._clean_email_content(email_content, context)
|
|
1336
1426
|
|
|
1337
1427
|
return cleaned_content
|
|
1338
1428
|
|
|
@@ -1443,35 +1533,74 @@ Generate 4 subject lines, one per line, no numbering or bullets:"""
|
|
|
1443
1533
|
f"5-minute chat about {company_name}?"
|
|
1444
1534
|
]
|
|
1445
1535
|
|
|
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
|
-
|
|
1536
|
+
def _clean_email_content(
|
|
1537
|
+
self,
|
|
1538
|
+
raw_content: str,
|
|
1539
|
+
context: Optional[Dict[str, Any]] = None
|
|
1540
|
+
) -> str:
|
|
1541
|
+
"""
|
|
1542
|
+
Clean and normalize generated email content, returning HTML.
|
|
1543
|
+
"""
|
|
1544
|
+
content = (raw_content or "").replace("\r\n", "\n").strip()
|
|
1545
|
+
|
|
1546
|
+
artifacts_to_remove = (
|
|
1547
|
+
"Here's the email:",
|
|
1548
|
+
"Here is the email:",
|
|
1549
|
+
"Email content:",
|
|
1550
|
+
"Generated email:",
|
|
1551
|
+
"Subject:",
|
|
1552
|
+
"Email:"
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
for artifact in artifacts_to_remove:
|
|
1556
|
+
if content.startswith(artifact):
|
|
1557
|
+
content = content[len(artifact):].strip()
|
|
1558
|
+
|
|
1559
|
+
lines = [line.strip() for line in content.split('\n') if line.strip()]
|
|
1560
|
+
content = '\n\n'.join(lines)
|
|
1561
|
+
|
|
1562
|
+
if not content:
|
|
1563
|
+
return "<html><body></body></html>"
|
|
1564
|
+
|
|
1565
|
+
if not content.lower().startswith(('dear ', 'hi ', 'hello ', 'greetings')):
|
|
1566
|
+
content = f"Dear Valued Customer,\n\n{content}"
|
|
1567
|
+
|
|
1568
|
+
closings = ('best regards', 'sincerely', 'thanks', 'thank you', 'kind regards')
|
|
1569
|
+
if not any(closing in content.lower() for closing in closings):
|
|
1570
|
+
content += "\n\nBest regards,\n[Your Name]"
|
|
1571
|
+
|
|
1572
|
+
ctx = context or {}
|
|
1573
|
+
input_data = ctx.get('input_data', {}) or {}
|
|
1574
|
+
rep_profile = getattr(self, '_active_rep_profile', {}) or {}
|
|
1575
|
+
staff_name = input_data.get('staff_name') or self.config.get('staff_name') or 'Sales Team'
|
|
1576
|
+
identity = ctx.get('_recipient_identity') or {}
|
|
1577
|
+
customer_first_name = (
|
|
1578
|
+
identity.get('first_name')
|
|
1579
|
+
or ctx.get('customer_first_name')
|
|
1580
|
+
or input_data.get('customer_first_name')
|
|
1581
|
+
or input_data.get('recipient_name')
|
|
1582
|
+
or input_data.get('customer_name')
|
|
1583
|
+
or ''
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1586
|
+
sanitized = self._sanitize_email_body(content, staff_name, rep_profile, customer_first_name)
|
|
1587
|
+
if '<html' not in sanitized.lower():
|
|
1588
|
+
sanitized = f"<html><body>{sanitized}</body></html>"
|
|
1589
|
+
|
|
1590
|
+
return sanitized
|
|
1591
|
+
|
|
1592
|
+
def _ensure_html_email(self, raw_content: Any, context: Dict[str, Any]) -> str:
|
|
1593
|
+
"""
|
|
1594
|
+
Normalize potentially plain-text content into HTML output.
|
|
1595
|
+
"""
|
|
1596
|
+
if raw_content is None:
|
|
1597
|
+
return "<html><body></body></html>"
|
|
1598
|
+
|
|
1599
|
+
text = str(raw_content)
|
|
1600
|
+
if '<html' in text.lower():
|
|
1601
|
+
return text
|
|
1602
|
+
|
|
1603
|
+
return self._clean_email_content(text, context)
|
|
1475
1604
|
|
|
1476
1605
|
def _extract_call_to_action(self, email_content: str) -> str:
|
|
1477
1606
|
"""Extract the main call-to-action from email content."""
|
|
@@ -1542,76 +1671,156 @@ Generate 4 subject lines, one per line, no numbering or bullets:"""
|
|
|
1542
1671
|
|
|
1543
1672
|
return min(score, 100)
|
|
1544
1673
|
|
|
1545
|
-
def _generate_template_email(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any],
|
|
1546
|
-
approach: Dict[str, Any], context: Dict[str, Any]) -> str:
|
|
1547
|
-
"""Generate email
|
|
1548
|
-
input_data = context.get('input_data', {})
|
|
1549
|
-
company_info = customer_data.get('companyInfo', {})
|
|
1550
|
-
contact_info = customer_data.get('primaryContact', {})
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1674
|
+
def _generate_template_email(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any],
|
|
1675
|
+
approach: Dict[str, Any], context: Dict[str, Any]) -> str:
|
|
1676
|
+
"""Generate a deterministic HTML email when LLM generation is unavailable."""
|
|
1677
|
+
input_data = context.get('input_data', {}) or {}
|
|
1678
|
+
company_info = customer_data.get('companyInfo', {}) or {}
|
|
1679
|
+
contact_info = customer_data.get('primaryContact', {}) or {}
|
|
1680
|
+
identity = context.get('_recipient_identity') or self._resolve_recipient_identity(customer_data, context)
|
|
1681
|
+
|
|
1682
|
+
first_name = identity.get('first_name') or contact_info.get('name') or input_data.get('customer_name') or input_data.get('recipient_name') or 'there'
|
|
1683
|
+
first_name = self._extract_first_name(first_name) if isinstance(first_name, str) else 'there'
|
|
1684
|
+
|
|
1685
|
+
staff_name = input_data.get('staff_name') or self.config.get('staff_name', 'Sales Team')
|
|
1686
|
+
org_name = input_data.get('org_name') or self.config.get('org_name', 'FuseSell')
|
|
1687
|
+
company_name = company_info.get('name', 'your company')
|
|
1688
|
+
industry = company_info.get('industry', 'your industry')
|
|
1689
|
+
approach_name = approach.get('name', 'professional_direct')
|
|
1690
|
+
approach_focus = approach.get('focus', 'business value')
|
|
1691
|
+
approach_tone = approach.get('tone', 'professional')
|
|
1692
|
+
|
|
1693
|
+
benefits: List[str] = []
|
|
1694
|
+
if recommended_product:
|
|
1695
|
+
product_name = recommended_product.get('product_name')
|
|
1696
|
+
benefits = [b for b in (recommended_product.get('key_benefits') or []) if b]
|
|
1697
|
+
if not benefits and product_name:
|
|
1698
|
+
benefits = [
|
|
1699
|
+
f"{product_name} accelerates {company_name}'s {approach_focus} goals",
|
|
1700
|
+
f"Designed specifically for {industry} operators",
|
|
1701
|
+
"Rapid onboarding with dedicated local support"
|
|
1702
|
+
]
|
|
1703
|
+
if not benefits:
|
|
1704
|
+
benefits = [
|
|
1705
|
+
f"Measurable improvements in {approach_focus}",
|
|
1706
|
+
f"Playbooks tailored for {industry} teams",
|
|
1707
|
+
"Guided adoption with FuseSell specialists"
|
|
1708
|
+
]
|
|
1709
|
+
|
|
1710
|
+
bullet_html = ''.join(f"<li>{benefit}</li>" for benefit in benefits)
|
|
1711
|
+
|
|
1712
|
+
cta_map = {
|
|
1713
|
+
'professional_direct': f"Would you have 20 minutes this week to explore how FuseSell can lighten {company_name}'s {approach_focus} workload?",
|
|
1714
|
+
'consultative': f"Could we schedule a short working session to dig into your current {approach_focus} priorities?",
|
|
1715
|
+
'industry_expert': f"Shall we review the latest {industry} benchmarks together and map them to your roadmap?",
|
|
1716
|
+
'relationship_building': f"I'd love to hear how your team is approaching {approach_focus}; is a quick virtual coffee an option?"
|
|
1717
|
+
}
|
|
1718
|
+
cta_text = cta_map.get(approach_name, f"Would you be open to a brief call to discuss {approach_focus} priorities at {company_name}?")
|
|
1719
|
+
|
|
1720
|
+
product_sentence = ""
|
|
1721
|
+
if recommended_product and recommended_product.get('product_name'):
|
|
1722
|
+
product_sentence = f"<p>We engineered <strong>{recommended_product['product_name']}</strong> specifically for teams tackling {approach_focus} in {industry}. It's a natural fit for {company_name}'s next phase.</p>"
|
|
1723
|
+
|
|
1724
|
+
news = company_info.get('recentNews')
|
|
1725
|
+
intro_sentence = f"<p>I'm reaching out because leaders at {company_name} are raising the same questions we hear from other {industry} innovators: how to keep {approach_focus} moving without burning out the team.</p>"
|
|
1726
|
+
if news:
|
|
1727
|
+
intro_sentence = f"<p>I noticed the recent update about {news}. Many {industry} peers use FuseSell to capitalise on moments exactly like this.</p>"
|
|
1728
|
+
|
|
1729
|
+
html = (
|
|
1730
|
+
"<html><body>"
|
|
1731
|
+
f"<p>Hi {first_name},</p>"
|
|
1732
|
+
f"{intro_sentence}"
|
|
1733
|
+
f"<p>From our {approach_tone.lower()} conversations with {industry} operators, three ideas could help {company_name} right away:</p>"
|
|
1734
|
+
f"<ul>{bullet_html}</ul>"
|
|
1735
|
+
f"{product_sentence}"
|
|
1736
|
+
f"<p>{cta_text}</p>"
|
|
1737
|
+
f"<p>Best regards,<br>{staff_name}<br>{org_name}</p>"
|
|
1738
|
+
"</body></html>"
|
|
1739
|
+
)
|
|
1740
|
+
|
|
1741
|
+
return html
|
|
1563
1742
|
|
|
1564
1743
|
def _generate_fallback_draft(self, customer_data: Dict[str, Any], recommended_product: Dict[str, Any], context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
1565
1744
|
"""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
|
-
|
|
1745
|
+
draft_id = f"uuid:{str(uuid.uuid4())}"
|
|
1746
|
+
recipient_identity = self._resolve_recipient_identity(customer_data, context)
|
|
1747
|
+
context.setdefault('_recipient_identity', recipient_identity)
|
|
1748
|
+
if recipient_identity.get('first_name') and not context.get('customer_first_name'):
|
|
1749
|
+
context['customer_first_name'] = recipient_identity['first_name']
|
|
1750
|
+
|
|
1751
|
+
template_body = self._generate_template_email(
|
|
1752
|
+
customer_data,
|
|
1753
|
+
recommended_product,
|
|
1754
|
+
{'tone': 'professional'},
|
|
1755
|
+
context
|
|
1756
|
+
)
|
|
1757
|
+
email_body = self._clean_email_content(template_body, context)
|
|
1758
|
+
|
|
1759
|
+
fallback_subjects = self._generate_fallback_subject_lines(customer_data, recommended_product)
|
|
1760
|
+
|
|
1761
|
+
return [{
|
|
1762
|
+
'draft_id': draft_id,
|
|
1763
|
+
'approach': 'fallback_template',
|
|
1764
|
+
'tone': 'professional',
|
|
1765
|
+
'focus': 'general outreach',
|
|
1766
|
+
'subject': fallback_subjects[0],
|
|
1767
|
+
'subject_alternatives': fallback_subjects[1:],
|
|
1768
|
+
'email_body': email_body,
|
|
1769
|
+
'email_format': 'html',
|
|
1770
|
+
'recipient_email': recipient_identity.get('email'),
|
|
1771
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
1772
|
+
'customer_first_name': recipient_identity.get('first_name'),
|
|
1773
|
+
'call_to_action': 'Would you be interested in a brief call?',
|
|
1774
|
+
'personalization_score': 50,
|
|
1775
|
+
'generated_at': datetime.now().isoformat(),
|
|
1776
|
+
'status': 'draft',
|
|
1777
|
+
'metadata': {
|
|
1778
|
+
'generation_method': 'template_fallback',
|
|
1779
|
+
'note': 'Generated using template due to LLM failure',
|
|
1780
|
+
'recipient_email': recipient_identity.get('email'),
|
|
1781
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
1782
|
+
'email_format': 'html'
|
|
1783
|
+
}
|
|
1784
|
+
}]
|
|
1587
1785
|
|
|
1588
1786
|
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
1787
|
"""Get mock email drafts for dry run."""
|
|
1590
1788
|
input_data = context.get('input_data', {})
|
|
1591
1789
|
company_info = customer_data.get('companyInfo', {})
|
|
1592
1790
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
'
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
'
|
|
1610
|
-
'
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1791
|
+
recipient_identity = self._resolve_recipient_identity(customer_data, context)
|
|
1792
|
+
context.setdefault('_recipient_identity', recipient_identity)
|
|
1793
|
+
mock_body = f"""[DRY RUN] Mock email content for {company_info.get('name', 'Test Company')}
|
|
1794
|
+
|
|
1795
|
+
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."""
|
|
1796
|
+
|
|
1797
|
+
return [{
|
|
1798
|
+
'draft_id': 'mock_draft_001',
|
|
1799
|
+
'approach': 'professional_direct',
|
|
1800
|
+
'tone': 'professional and direct',
|
|
1801
|
+
'focus': 'business value and ROI',
|
|
1802
|
+
'subject': f"Partnership Opportunity for {company_info.get('name', 'Test Company')}",
|
|
1803
|
+
'subject_alternatives': [
|
|
1804
|
+
f"Quick Question About {company_info.get('name', 'Test Company')}",
|
|
1805
|
+
f"Helping Companies Like {company_info.get('name', 'Test Company')}"
|
|
1806
|
+
],
|
|
1807
|
+
'email_body': self._clean_email_content(mock_body, context),
|
|
1808
|
+
'email_format': 'html',
|
|
1809
|
+
'recipient_email': recipient_identity.get('email'),
|
|
1810
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
1811
|
+
'customer_first_name': recipient_identity.get('first_name'),
|
|
1812
|
+
'call_to_action': 'Mock call to action',
|
|
1813
|
+
'personalization_score': 85,
|
|
1814
|
+
'generated_at': datetime.now().isoformat(),
|
|
1815
|
+
'status': 'mock',
|
|
1816
|
+
'metadata': {
|
|
1817
|
+
'generation_method': 'mock_data',
|
|
1818
|
+
'note': 'This is mock data for dry run testing',
|
|
1819
|
+
'recipient_email': recipient_identity.get('email'),
|
|
1820
|
+
'recipient_name': recipient_identity.get('full_name'),
|
|
1821
|
+
'email_format': 'html'
|
|
1822
|
+
}
|
|
1823
|
+
}]
|
|
1615
1824
|
|
|
1616
1825
|
|
|
1617
1826
|
def _convert_draft_to_server_format(self, draft: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -1950,16 +2159,17 @@ This is a mock email that would be generated for testing purposes. In a real exe
|
|
|
1950
2159
|
|
|
1951
2160
|
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
2161
|
"""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
|
-
|
|
2162
|
+
try:
|
|
2163
|
+
if self.is_dry_run():
|
|
2164
|
+
rewritten = existing_draft.copy()
|
|
2165
|
+
rewritten['draft_id'] = f"uuid:{str(uuid.uuid4())}"
|
|
2166
|
+
rewritten['draft_approach'] = "rewrite"
|
|
2167
|
+
rewritten['draft_type'] = "rewrite"
|
|
2168
|
+
rewritten['email_body'] = f"[DRY RUN - REWRITTEN: {reason}] " + existing_draft.get('email_body', '')
|
|
2169
|
+
rewritten['rewrite_reason'] = reason
|
|
2170
|
+
rewritten['rewritten_at'] = datetime.now().isoformat()
|
|
2171
|
+
rewritten.setdefault('email_format', 'html')
|
|
2172
|
+
return rewritten
|
|
1963
2173
|
|
|
1964
2174
|
input_data = context.get('input_data', {})
|
|
1965
2175
|
company_info = customer_data.get('companyInfo', {})
|
|
@@ -1997,46 +2207,66 @@ Generate only the rewritten email content:"""
|
|
|
1997
2207
|
)
|
|
1998
2208
|
|
|
1999
2209
|
# 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)
|
|
2210
|
+
cleaned_content = self._clean_email_content(rewritten_content, context)
|
|
2211
|
+
|
|
2212
|
+
# Create rewritten draft object
|
|
2213
|
+
rewritten = existing_draft.copy()
|
|
2214
|
+
rewritten['draft_id'] = f"uuid:{str(uuid.uuid4())}"
|
|
2215
|
+
rewritten['draft_approach'] = "rewrite"
|
|
2216
|
+
rewritten['draft_type'] = "rewrite"
|
|
2217
|
+
rewritten['email_body'] = cleaned_content
|
|
2218
|
+
rewritten['rewrite_reason'] = reason
|
|
2219
|
+
rewritten['rewritten_at'] = datetime.now().isoformat()
|
|
2220
|
+
rewritten['version'] = existing_draft.get('version', 1) + 1
|
|
2221
|
+
rewritten['call_to_action'] = self._extract_call_to_action(cleaned_content)
|
|
2222
|
+
rewritten['personalization_score'] = self._calculate_personalization_score(cleaned_content, customer_data)
|
|
2223
|
+
recipient_identity = context.get('_recipient_identity') or self._resolve_recipient_identity(customer_data, context)
|
|
2224
|
+
if recipient_identity:
|
|
2225
|
+
rewritten['recipient_email'] = recipient_identity.get('email')
|
|
2226
|
+
rewritten['recipient_name'] = recipient_identity.get('full_name')
|
|
2227
|
+
rewritten['customer_first_name'] = recipient_identity.get('first_name')
|
|
2228
|
+
rewritten['email_format'] = 'html'
|
|
2013
2229
|
|
|
2014
2230
|
# Update metadata
|
|
2015
2231
|
if 'metadata' not in rewritten:
|
|
2016
2232
|
rewritten['metadata'] = {}
|
|
2017
2233
|
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'
|
|
2234
|
+
rewritten['metadata']['rewrite_history'].append({
|
|
2235
|
+
'reason': reason,
|
|
2236
|
+
'rewritten_at': datetime.now().isoformat(),
|
|
2237
|
+
'original_draft_id': existing_draft.get('draft_id')
|
|
2238
|
+
})
|
|
2239
|
+
rewritten['metadata']['generation_method'] = 'llm_rewrite'
|
|
2240
|
+
if recipient_identity:
|
|
2241
|
+
rewritten['metadata']['recipient_email'] = recipient_identity.get('email')
|
|
2242
|
+
rewritten['metadata']['recipient_name'] = recipient_identity.get('full_name')
|
|
2243
|
+
rewritten['metadata']['email_format'] = 'html'
|
|
2024
2244
|
|
|
2025
2245
|
self.logger.info(f"Successfully rewrote draft based on reason: {reason}")
|
|
2026
2246
|
return rewritten
|
|
2027
2247
|
|
|
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
|
-
|
|
2248
|
+
except Exception as e:
|
|
2249
|
+
self.logger.error(f"Failed to rewrite draft: {str(e)}")
|
|
2250
|
+
# Fallback to simple modification
|
|
2251
|
+
rewritten = existing_draft.copy()
|
|
2252
|
+
rewritten['draft_id'] = f"uuid:{str(uuid.uuid4())}"
|
|
2253
|
+
rewritten['draft_approach'] = "rewrite"
|
|
2254
|
+
rewritten['draft_type'] = "rewrite"
|
|
2255
|
+
rewritten['email_body'] = f"[REWRITTEN: {reason}]\n\n" + existing_draft.get('email_body', '')
|
|
2256
|
+
rewritten['rewrite_reason'] = reason
|
|
2257
|
+
rewritten['rewritten_at'] = datetime.now().isoformat()
|
|
2258
|
+
rewritten['metadata'] = {'generation_method': 'template_rewrite', 'error': str(e)}
|
|
2259
|
+
rewritten['email_body'] = self._clean_email_content(rewritten['email_body'], context)
|
|
2260
|
+
fallback_identity = context.get('_recipient_identity') or self._resolve_recipient_identity(customer_data, context)
|
|
2261
|
+
if fallback_identity:
|
|
2262
|
+
rewritten['recipient_email'] = fallback_identity.get('email')
|
|
2263
|
+
rewritten['recipient_name'] = fallback_identity.get('full_name')
|
|
2264
|
+
rewritten['customer_first_name'] = fallback_identity.get('first_name')
|
|
2265
|
+
rewritten['metadata']['recipient_email'] = fallback_identity.get('email')
|
|
2266
|
+
rewritten['metadata']['recipient_name'] = fallback_identity.get('full_name')
|
|
2267
|
+
rewritten['metadata']['email_format'] = 'html'
|
|
2268
|
+
rewritten['email_format'] = 'html'
|
|
2269
|
+
return rewritten
|
|
2040
2270
|
|
|
2041
2271
|
def _save_rewritten_draft(self, context: Dict[str, Any], rewritten_draft: Dict[str, Any], original_draft_id: str) -> Dict[str, Any]:
|
|
2042
2272
|
"""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
|