Spaces:
Running
Running
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
|