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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fusesell
3
- Version: 1.2.9
3
+ Version: 1.3.0
4
4
  Summary: Local implementation of FuseSell AI sales automation pipeline
5
5
  Author-email: FuseSell Team <team@fusesell.ai>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  fusesell.py,sha256=t5PjkhWEJGINp4k517u0EX0ge7lzuHOUHHro-BE1mGk,596
2
- fusesell-1.2.9.dist-info/licenses/LICENSE,sha256=GDz1ZoC4lB0kwjERpzqc_OdA_awYVso2aBnUH-ErW_w,1070
3
- fusesell_local/__init__.py,sha256=Ajbk9gus0-G2gR6WAo1F_QTApGpLzz01I2LL2iE6t8c,967
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=yoXAVaPgQXZc3bMq4U363Z4ARTsnSzOqbodKB3tke3A,123593
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=60CLOVkVB76AQx1wQyja0PFmA1t-YJITiGNni14IPOs,188449
26
- fusesell_local/utils/event_scheduler.py,sha256=tP-rnx9Hixfcm6ZTqloLy_EgSPII89v5dSycRHrCTLE,39824
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.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,,
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,,
@@ -32,6 +32,6 @@ __all__ = [
32
32
  "validate_config",
33
33
  ]
34
34
 
35
- __version__ = "1.2.9"
35
+ __version__ = "1.3.0"
36
36
  __author__ = "FuseSell Team"
37
37
  __description__ = "Local implementation of FuseSell AI sales automation pipeline"
@@ -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
- # Rewrite the draft based on reason
176
- rewritten_draft = self._rewrite_draft(existing_draft, reason, customer_data, scoring_data, context)
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 _get_auto_interaction_config(self, team_id: str = None) -> Dict[str, Any]:
591
- """
592
- Get auto interaction configuration from team settings.
593
-
594
- Args:
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
- 'call_to_action': self._extract_call_to_action(email_content),
737
- 'personalization_score': self._calculate_personalization_score(email_content, customer_data),
738
- 'generated_at': datetime.now().isoformat(),
739
- 'status': 'draft',
740
- 'metadata': {
741
- 'customer_company': company_info.get('name', 'Unknown'),
742
- 'contact_name': contact_info.get('name', 'Unknown'),
743
- 'recommended_product': recommended_product.get('product_name', 'Unknown'),
744
- 'pain_points_addressed': len([p for p in pain_points if p.get('severity') in ['high', 'medium']]),
745
- 'generation_method': 'llm_powered'
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
- subject = entry.get('subject')
1205
- if isinstance(subject, list):
1206
- subject = subject[0] if subject else ''
1207
- subject = str(subject).strip() if subject else ''
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
- metadata = {
1257
- 'customer_company': customer_data.get('companyInfo', {}).get('name', 'Unknown'),
1258
- 'contact_name': customer_data.get('primaryContact', {}).get('name', 'Unknown'),
1259
- 'recommended_product': product_name or 'Unknown',
1260
- 'generation_method': 'prompt_template',
1261
- 'tags': tags,
1262
- 'message_type': message_type
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
- 'call_to_action': call_to_action,
1278
- 'product_mention': product_mention,
1279
- 'product_name': product_name,
1280
- 'priority_order': priority_order if priority_order is not None else 0,
1281
- 'personalization_score': personalization_score,
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(self, raw_content: str) -> str:
1447
- """Clean and validate generated email content."""
1448
- # Remove any unwanted prefixes or suffixes
1449
- content = raw_content.strip()
1450
-
1451
- # Remove common LLM artifacts
1452
- artifacts_to_remove = [
1453
- "Here's the email:",
1454
- "Here is the email:",
1455
- "Email content:",
1456
- "Generated email:",
1457
- "Subject:",
1458
- "Email:"
1459
- ]
1460
-
1461
- for artifact in artifacts_to_remove:
1462
- if content.startswith(artifact):
1463
- content = content[len(artifact):].strip()
1464
-
1465
- # Ensure proper email structure
1466
- if not content.startswith(('Dear', 'Hi', 'Hello', 'Greetings')):
1467
- # Add a greeting if missing
1468
- content = f"Dear Valued Customer,\n\n{content}"
1469
-
1470
- # Ensure proper closing
1471
- if not any(closing in content.lower() for closing in ['best regards', 'sincerely', 'best', 'thanks']):
1472
- content += "\n\nBest regards"
1473
-
1474
- return content
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
- draft_approach = "fallback"
1568
- draft_type = "initial"
1569
-
1570
- return [{
1571
- 'draft_id': draft_id,
1572
- 'approach': 'fallback_template',
1573
- 'tone': 'professional',
1574
- 'focus': 'general outreach',
1575
- 'subject': self._generate_fallback_subject_lines(customer_data, recommended_product)[0],
1576
- 'subject_alternatives': self._generate_fallback_subject_lines(customer_data, recommended_product)[1:],
1577
- 'email_body': self._generate_template_email(customer_data, recommended_product, {'tone': 'professional'}, context),
1578
- 'call_to_action': 'Would you be interested in a brief call?',
1579
- 'personalization_score': 50,
1580
- 'generated_at': datetime.now().isoformat(),
1581
- 'status': 'draft',
1582
- 'metadata': {
1583
- 'generation_method': 'template_fallback',
1584
- 'note': 'Generated using template due to LLM failure'
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
- return [{
1594
- 'draft_id': 'mock_draft_001',
1595
- 'approach': 'professional_direct',
1596
- 'tone': 'professional and direct',
1597
- 'focus': 'business value and ROI',
1598
- 'subject': f"Partnership Opportunity for {company_info.get('name', 'Test Company')}",
1599
- 'subject_alternatives': [
1600
- f"Quick Question About {company_info.get('name', 'Test Company')}",
1601
- f"Helping Companies Like {company_info.get('name', 'Test Company')}"
1602
- ],
1603
- 'email_body': f"""[DRY RUN] Mock email content for {company_info.get('name', 'Test Company')}
1604
-
1605
- 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.""",
1606
- 'call_to_action': 'Mock call to action',
1607
- 'personalization_score': 85,
1608
- 'generated_at': datetime.now().isoformat(),
1609
- 'status': 'mock',
1610
- 'metadata': {
1611
- 'generation_method': 'mock_data',
1612
- 'note': 'This is mock data for dry run testing'
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
- return rewritten
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
- return rewritten
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