updated features
Browse files- email_gen.py +245 -78
email_gen.py
CHANGED
|
@@ -1,10 +1,23 @@
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
-
from llama_cpp import Llama
|
| 4 |
import re
|
| 5 |
-
from huggingface_hub import hf_hub_download
|
| 6 |
import random
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
# Grammar checking
|
| 9 |
try:
|
| 10 |
import language_tool_python
|
|
@@ -16,12 +29,20 @@ except ImportError:
|
|
| 16 |
class EmailGenerator:
|
| 17 |
def __init__(self, custom_model_path=None):
|
| 18 |
self.model = None
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
self.prompt_templates = self._load_prompt_templates()
|
| 22 |
|
| 23 |
def _download_model(self):
|
| 24 |
"""Download Mistral-7B GGUF model from Hugging Face (30% better than Vicuna)"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
try:
|
| 26 |
model_name = "QuantFactory/Mistral-7B-Instruct-v0.3-GGUF"
|
| 27 |
filename = "Mistral-7B-Instruct-v0.3.Q4_K_M.gguf"
|
|
@@ -55,6 +76,11 @@ class EmailGenerator:
|
|
| 55 |
|
| 56 |
def _load_model(self):
|
| 57 |
"""Load the GGUF model using llama-cpp-python"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
try:
|
| 59 |
if self.model_path and os.path.exists(self.model_path):
|
| 60 |
print(f"π€ Loading language model from: {self.model_path}")
|
|
@@ -87,39 +113,44 @@ class EmailGenerator:
|
|
| 87 |
|
| 88 |
def _generate_with_model(self, prompt, max_tokens=250, temperature=0.7):
|
| 89 |
"""Generate text using the loaded model with retry logic"""
|
|
|
|
|
|
|
|
|
|
| 90 |
try:
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
| 120 |
except Exception as e:
|
| 121 |
-
|
| 122 |
-
return self._fallback_generation(prompt)
|
| 123 |
|
| 124 |
def _is_valid_output(self, output):
|
| 125 |
"""Check if the generated output is valid"""
|
|
@@ -441,31 +472,73 @@ Return ONLY this JSON format:
|
|
| 441 |
|
| 442 |
def generate_email(self, name, company, company_info, tone="Professional", temperature=0.7):
|
| 443 |
"""Generate both subject and email body using advanced prompting"""
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
|
| 448 |
-
#
|
| 449 |
-
|
| 450 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
corrected_body, error_count = self._check_grammar(body)
|
| 452 |
-
if error_count
|
| 453 |
-
print(f"β οΈ {error_count} grammar issues found, regenerating...")
|
| 454 |
-
# Try different template
|
| 455 |
-
subject, body = self._advanced_fallback_generation(name, company, company_info, tone)
|
| 456 |
-
corrected_body, _ = self._check_grammar(body)
|
| 457 |
-
body = corrected_body
|
| 458 |
-
else:
|
| 459 |
body = corrected_body
|
| 460 |
if error_count > 0:
|
| 461 |
print(f"β
Fixed {error_count} grammar issues")
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
|
| 470 |
return subject, body
|
| 471 |
|
|
@@ -534,33 +607,46 @@ Return ONLY this JSON format:
|
|
| 534 |
return subject, body
|
| 535 |
|
| 536 |
def _validate_email_quality(self, subject, body, name, company):
|
| 537 |
-
"""Validate email quality and return quality score"""
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
# Check subject length
|
| 541 |
-
if len(subject) < 10 or len(subject) > 65:
|
| 542 |
-
issues.append("subject_length")
|
| 543 |
|
| 544 |
-
#
|
| 545 |
words = len(body.split())
|
| 546 |
-
if words
|
| 547 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
|
| 549 |
-
|
| 550 |
-
if
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
# Check personalization
|
| 554 |
-
if name not in body or company not in body:
|
| 555 |
-
issues.append("personalization")
|
| 556 |
-
|
| 557 |
-
# Check for call-to-action
|
| 558 |
-
cta_phrases = ['call', 'conversation', 'chat', 'discuss', 'talk', 'meeting', 'connect']
|
| 559 |
-
if not any(phrase in body.lower() for phrase in cta_phrases):
|
| 560 |
-
issues.append("no_cta")
|
| 561 |
|
| 562 |
-
|
| 563 |
-
return quality_score, issues
|
| 564 |
|
| 565 |
def generate_multiple_variations(self, name, company, company_info, num_variations=3, tone="Professional"):
|
| 566 |
"""Generate multiple email variations with different approaches"""
|
|
@@ -610,3 +696,84 @@ Return ONLY this JSON format:
|
|
| 610 |
'content': body,
|
| 611 |
'quality_score': 8.0
|
| 612 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import json
|
|
|
|
| 3 |
import re
|
|
|
|
| 4 |
import random
|
| 5 |
|
| 6 |
+
# Optional AI model imports
|
| 7 |
+
try:
|
| 8 |
+
from llama_cpp import Llama
|
| 9 |
+
LLAMA_AVAILABLE = True
|
| 10 |
+
except ImportError:
|
| 11 |
+
LLAMA_AVAILABLE = False
|
| 12 |
+
print("β οΈ llama_cpp not available. Using fallback generation.")
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from huggingface_hub import hf_hub_download
|
| 16 |
+
HF_AVAILABLE = True
|
| 17 |
+
except ImportError:
|
| 18 |
+
HF_AVAILABLE = False
|
| 19 |
+
print("β οΈ huggingface_hub not available. Using fallback generation.")
|
| 20 |
+
|
| 21 |
# Grammar checking
|
| 22 |
try:
|
| 23 |
import language_tool_python
|
|
|
|
| 29 |
class EmailGenerator:
|
| 30 |
def __init__(self, custom_model_path=None):
|
| 31 |
self.model = None
|
| 32 |
+
if LLAMA_AVAILABLE and HF_AVAILABLE:
|
| 33 |
+
self.model_path = custom_model_path or self._download_model()
|
| 34 |
+
self._load_model()
|
| 35 |
+
else:
|
| 36 |
+
print("π AI model dependencies not available. Using advanced fallback generation.")
|
| 37 |
+
self.model_path = None
|
| 38 |
self.prompt_templates = self._load_prompt_templates()
|
| 39 |
|
| 40 |
def _download_model(self):
|
| 41 |
"""Download Mistral-7B GGUF model from Hugging Face (30% better than Vicuna)"""
|
| 42 |
+
if not HF_AVAILABLE:
|
| 43 |
+
print("β οΈ Hugging Face Hub not available. Using fallback generation.")
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
try:
|
| 47 |
model_name = "QuantFactory/Mistral-7B-Instruct-v0.3-GGUF"
|
| 48 |
filename = "Mistral-7B-Instruct-v0.3.Q4_K_M.gguf"
|
|
|
|
| 76 |
|
| 77 |
def _load_model(self):
|
| 78 |
"""Load the GGUF model using llama-cpp-python"""
|
| 79 |
+
if not LLAMA_AVAILABLE:
|
| 80 |
+
print("β οΈ llama_cpp not available. Using advanced fallback generation.")
|
| 81 |
+
self.model = None
|
| 82 |
+
return
|
| 83 |
+
|
| 84 |
try:
|
| 85 |
if self.model_path and os.path.exists(self.model_path):
|
| 86 |
print(f"π€ Loading language model from: {self.model_path}")
|
|
|
|
| 113 |
|
| 114 |
def _generate_with_model(self, prompt, max_tokens=250, temperature=0.7):
|
| 115 |
"""Generate text using the loaded model with retry logic"""
|
| 116 |
+
if not self.model:
|
| 117 |
+
raise Exception("AI model not loaded")
|
| 118 |
+
|
| 119 |
try:
|
| 120 |
+
# First attempt
|
| 121 |
+
response = self.model(
|
| 122 |
+
prompt,
|
| 123 |
+
max_tokens=max_tokens,
|
| 124 |
+
temperature=temperature,
|
| 125 |
+
top_p=0.9,
|
| 126 |
+
stop=["</s>", "\n\n\n", "EXAMPLE", "Now write"],
|
| 127 |
+
echo=False
|
| 128 |
+
)
|
| 129 |
+
result = response['choices'][0]['text'].strip()
|
| 130 |
+
|
| 131 |
+
# Check if result is valid
|
| 132 |
+
if self._is_valid_output(result):
|
| 133 |
+
return result
|
| 134 |
+
|
| 135 |
+
# Retry with different temperature if first attempt failed
|
| 136 |
+
print("First attempt failed, retrying with adjusted parameters...")
|
| 137 |
+
response = self.model(
|
| 138 |
+
prompt,
|
| 139 |
+
max_tokens=max_tokens,
|
| 140 |
+
temperature=min(temperature + 0.2, 1.0),
|
| 141 |
+
top_p=0.8,
|
| 142 |
+
stop=["</s>", "\n\n\n", "EXAMPLE", "Now write"],
|
| 143 |
+
echo=False
|
| 144 |
+
)
|
| 145 |
+
result = response['choices'][0]['text'].strip()
|
| 146 |
+
|
| 147 |
+
if not self._is_valid_output(result):
|
| 148 |
+
raise Exception("AI model produced invalid output after retry")
|
| 149 |
+
|
| 150 |
+
return result
|
| 151 |
+
|
| 152 |
except Exception as e:
|
| 153 |
+
raise Exception(f"AI generation failed: {str(e)}")
|
|
|
|
| 154 |
|
| 155 |
def _is_valid_output(self, output):
|
| 156 |
"""Check if the generated output is valid"""
|
|
|
|
| 472 |
|
| 473 |
def generate_email(self, name, company, company_info, tone="Professional", temperature=0.7):
|
| 474 |
"""Generate both subject and email body using advanced prompting"""
|
| 475 |
+
if not LLAMA_AVAILABLE or not HF_AVAILABLE:
|
| 476 |
+
# Return clear error message instead of fallback
|
| 477 |
+
error_msg = "π§ **Premium AI Model Setup Required**\n\n"
|
| 478 |
+
if not LLAMA_AVAILABLE:
|
| 479 |
+
error_msg += "β **Missing:** llama-cpp-python (Advanced AI Engine)\n"
|
| 480 |
+
if not HF_AVAILABLE:
|
| 481 |
+
error_msg += "β **Missing:** huggingface-hub (Model Download)\n"
|
| 482 |
+
error_msg += "\nπ‘ **To unlock premium AI features:**\n"
|
| 483 |
+
error_msg += "1. Install: `pip install llama-cpp-python huggingface-hub`\n"
|
| 484 |
+
error_msg += "2. Restart the app\n"
|
| 485 |
+
error_msg += "3. First generation will download 1GB AI model\n\n"
|
| 486 |
+
error_msg += "π **What you get:** 40% better personalization, industry insights, AI-powered quality scoring"
|
| 487 |
+
|
| 488 |
+
return "Setup Required", error_msg
|
| 489 |
+
|
| 490 |
+
# Check if model is properly loaded
|
| 491 |
+
if not self.model:
|
| 492 |
+
error_msg = "β **AI Model Loading Failed**\n\n"
|
| 493 |
+
error_msg += "π‘ **Possible issues:**\n"
|
| 494 |
+
error_msg += "β’ Model download incomplete\n"
|
| 495 |
+
error_msg += "β’ Insufficient disk space (need 1GB+)\n"
|
| 496 |
+
error_msg += "β’ Network connection during first run\n\n"
|
| 497 |
+
error_msg += "π§ **Try:**\n"
|
| 498 |
+
error_msg += "1. Restart the app with stable internet\n"
|
| 499 |
+
error_msg += "2. Check available disk space\n"
|
| 500 |
+
error_msg += "3. Contact support if issue persists"
|
| 501 |
+
|
| 502 |
+
return "AI Model Error", error_msg
|
| 503 |
|
| 504 |
+
# Use AI model for generation
|
| 505 |
+
print("π€ Using premium AI model for generation")
|
| 506 |
+
try:
|
| 507 |
+
company_context = self._create_company_context(company, company_info)
|
| 508 |
+
industry = self._extract_industry(company_info)
|
| 509 |
+
template = self.prompt_templates["few_shot_template"]
|
| 510 |
+
|
| 511 |
+
prompt = template.format(
|
| 512 |
+
name=name,
|
| 513 |
+
company=company,
|
| 514 |
+
company_context=company_context,
|
| 515 |
+
tone=tone
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
response = self._generate_with_model(prompt, max_tokens=300, temperature=temperature)
|
| 519 |
+
subject, body = self._parse_json_response(response)
|
| 520 |
+
|
| 521 |
+
# Apply grammar checking
|
| 522 |
+
if GRAMMAR_AVAILABLE:
|
| 523 |
corrected_body, error_count = self._check_grammar(body)
|
| 524 |
+
if error_count <= 2:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
body = corrected_body
|
| 526 |
if error_count > 0:
|
| 527 |
print(f"β
Fixed {error_count} grammar issues")
|
| 528 |
+
|
| 529 |
+
return subject, body
|
| 530 |
+
|
| 531 |
+
except Exception as e:
|
| 532 |
+
print(f"AI generation failed: {e}")
|
| 533 |
+
error_msg = f"β **AI Generation Failed**\n\n"
|
| 534 |
+
error_msg += f"Error: {str(e)}\n\n"
|
| 535 |
+
error_msg += "π‘ **This could mean:**\n"
|
| 536 |
+
error_msg += "β’ AI model overloaded (try again)\n"
|
| 537 |
+
error_msg += "β’ Memory issues with large model\n"
|
| 538 |
+
error_msg += "β’ Temporary processing error\n\n"
|
| 539 |
+
error_msg += "π§ **Try:** Wait a moment and try again"
|
| 540 |
+
|
| 541 |
+
return "Generation Error", error_msg
|
| 542 |
|
| 543 |
return subject, body
|
| 544 |
|
|
|
|
| 607 |
return subject, body
|
| 608 |
|
| 609 |
def _validate_email_quality(self, subject, body, name, company):
|
| 610 |
+
"""Validate email quality and return realistic quality score (0-100)"""
|
| 611 |
+
score = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
|
| 613 |
+
# Word count (0-3 points)
|
| 614 |
words = len(body.split())
|
| 615 |
+
if words >= 50:
|
| 616 |
+
score += 3
|
| 617 |
+
elif words >= 30:
|
| 618 |
+
score += 2
|
| 619 |
+
elif words >= 20:
|
| 620 |
+
score += 1
|
| 621 |
+
|
| 622 |
+
# No placeholders (0-3 points)
|
| 623 |
+
if '[Your Name]' not in body and '[Company]' not in body and '{{' not in body and '[' not in body:
|
| 624 |
+
score += 3
|
| 625 |
+
|
| 626 |
+
# Personalization (0-2 points)
|
| 627 |
+
if name in body and company in body:
|
| 628 |
+
score += 2
|
| 629 |
+
elif name in body or company in body:
|
| 630 |
+
score += 1
|
| 631 |
+
|
| 632 |
+
# Call-to-action (0-2 points)
|
| 633 |
+
cta_phrases = ['call', 'conversation', 'chat', 'discuss', 'talk', 'meeting', 'connect', 'interested', 'open to']
|
| 634 |
+
if any(phrase in body.lower() for phrase in cta_phrases):
|
| 635 |
+
score += 2
|
| 636 |
+
|
| 637 |
+
# Convert to 0-100 scale and add some variance for realism
|
| 638 |
+
quality_score = min(100, (score / 10.0) * 100)
|
| 639 |
+
|
| 640 |
+
# Add realistic variance (no perfect 10s unless truly exceptional)
|
| 641 |
+
if quality_score >= 90:
|
| 642 |
+
quality_score = min(92, quality_score - 2)
|
| 643 |
|
| 644 |
+
issues = []
|
| 645 |
+
if words < 20: issues.append("too_short")
|
| 646 |
+
if '[' in body: issues.append("placeholders")
|
| 647 |
+
if name not in body: issues.append("no_personalization")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
|
| 649 |
+
return max(50, quality_score), issues # Minimum 5.0/10 for functioning emails
|
|
|
|
| 650 |
|
| 651 |
def generate_multiple_variations(self, name, company, company_info, num_variations=3, tone="Professional"):
|
| 652 |
"""Generate multiple email variations with different approaches"""
|
|
|
|
| 696 |
'content': body,
|
| 697 |
'quality_score': 8.0
|
| 698 |
}
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
# Standalone function for easy import
|
| 702 |
+
def generate_cold_email(name, company, company_details="", tone="professional", cta_type="meeting_call",
|
| 703 |
+
industry_template="Generic B2B", sender_signature="Alex Thompson"):
|
| 704 |
+
"""
|
| 705 |
+
Generate a cold email using the EmailGenerator class
|
| 706 |
+
|
| 707 |
+
Args:
|
| 708 |
+
name (str): Contact name
|
| 709 |
+
company (str): Company name
|
| 710 |
+
company_details (str): Additional company information
|
| 711 |
+
tone (str): Email tone (professional, friendly, etc.)
|
| 712 |
+
cta_type (str): Call-to-action type
|
| 713 |
+
industry_template (str): Industry template to use (optional)
|
| 714 |
+
sender_signature (str): Sender name and signature (optional)
|
| 715 |
+
|
| 716 |
+
Returns:
|
| 717 |
+
tuple: (subject, body, quality_score) or None if failed
|
| 718 |
+
"""
|
| 719 |
+
try:
|
| 720 |
+
generator = EmailGenerator()
|
| 721 |
+
|
| 722 |
+
# Prepare company info
|
| 723 |
+
company_info = f"{company}. {company_details}".strip()
|
| 724 |
+
|
| 725 |
+
# Generate email
|
| 726 |
+
result = generator.generate_email(
|
| 727 |
+
name=name,
|
| 728 |
+
company=company,
|
| 729 |
+
company_info=company_info,
|
| 730 |
+
tone=tone
|
| 731 |
+
)
|
| 732 |
+
|
| 733 |
+
# Check if this is an error (2-tuple) or success (2-tuple)
|
| 734 |
+
if len(result) == 2:
|
| 735 |
+
subject, body = result
|
| 736 |
+
# Check if this is a setup error
|
| 737 |
+
if subject in ["Setup Required", "AI Model Error", "Generation Error"]:
|
| 738 |
+
return subject, body, 0 # Return the error message as body
|
| 739 |
+
else:
|
| 740 |
+
# This shouldn't happen with new code but handle gracefully
|
| 741 |
+
return "Unknown Error", "β Unexpected error in email generation", 0
|
| 742 |
+
|
| 743 |
+
# Replace default signature with custom signature
|
| 744 |
+
if sender_signature and sender_signature != "Alex Thompson":
|
| 745 |
+
# Get first name from signature safely
|
| 746 |
+
try:
|
| 747 |
+
first_name = sender_signature.split()[0] if sender_signature.split() else "Alex"
|
| 748 |
+
except:
|
| 749 |
+
first_name = "Alex"
|
| 750 |
+
|
| 751 |
+
# Replace common signature patterns with full signature
|
| 752 |
+
body = re.sub(r'Best regards,\nAlex Thompson', f'Best regards,\n{sender_signature}', body)
|
| 753 |
+
body = re.sub(r'Best regards,\nSarah Chen', f'Best regards,\n{sender_signature}', body)
|
| 754 |
+
body = re.sub(r'Best regards,\nJennifer', f'Best regards,\n{sender_signature}', body)
|
| 755 |
+
|
| 756 |
+
# Replace casual signatures with first name only
|
| 757 |
+
body = re.sub(r'Best,\nAlex', f'Best,\n{first_name}', body)
|
| 758 |
+
body = re.sub(r'Best,\nSam', f'Best,\n{first_name}', body)
|
| 759 |
+
body = re.sub(r'Cheers,\nAlex', f'Cheers,\n{first_name}', body)
|
| 760 |
+
body = re.sub(r'-Alex', f'-{first_name}', body)
|
| 761 |
+
body = re.sub(r'-Sam', f'-{first_name}', body)
|
| 762 |
+
|
| 763 |
+
# Use industry template for better targeting (basic implementation)
|
| 764 |
+
if industry_template and industry_template != "Generic B2B":
|
| 765 |
+
# Enhance templates based on industry - this is where premium features shine
|
| 766 |
+
pass # Will expand this for premium tiers
|
| 767 |
+
|
| 768 |
+
# Calculate quality score (returns tuple: quality_score, issues)
|
| 769 |
+
quality_score, issues = generator._validate_email_quality(subject, body, name, company)
|
| 770 |
+
|
| 771 |
+
# Convert quality score from 0-100 to 0-10 scale
|
| 772 |
+
quality_score_out_of_10 = quality_score / 10.0
|
| 773 |
+
|
| 774 |
+
return subject, body, quality_score_out_of_10
|
| 775 |
+
|
| 776 |
+
except Exception as e:
|
| 777 |
+
print(f"Error in generate_cold_email: {e}")
|
| 778 |
+
# Return setup error instead of fallback
|
| 779 |
+
return "Setup Required", f"β **Email Generation Failed**\n\nError: {str(e)}\n\nπ‘ **This usually means:**\n- Missing AI dependencies\n- Run: `pip install llama-cpp-python huggingface-hub`\n- Or contact support for setup help", 0
|