feat: Enhanced Outreach UI - Top 5 default + specific role generation

- market_intel_orchestrator.py: Updated generate_outreach_campaign to identify all relevant roles, generate top 5, and return remaining as suggestions. Added specific_role mode.

- types.ts: Added OutreachResponse interface.

- geminiService.ts: Updated to handle new response structure and specificRole parameter.

- StepOutreach.tsx: Added sidebar section for Suggested Roles with on-demand generation buttons.
This commit is contained in:
2025-12-29 13:59:20 +00:00
parent 4e05bac827
commit 4b79f1aee2
4 changed files with 211 additions and 125 deletions

View File

@@ -1,8 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { AnalysisResult, EmailDraft, Language } from '../types'; import { AnalysisResult, EmailDraft, Language, OutreachResponse } from '../types';
import { generateOutreachCampaign, translateEmailDrafts } from '../services/geminiService'; import { generateOutreachCampaign, translateEmailDrafts } from '../services/geminiService';
import { Upload, FileText, Sparkles, Copy, Check, Loader2, ArrowRight, CheckCircle2, Download, Languages } from 'lucide-react'; import { Upload, FileText, Sparkles, Copy, Check, Loader2, ArrowRight, CheckCircle2, Download, Languages, Plus, UserPlus } from 'lucide-react';
interface StepOutreachProps { interface StepOutreachProps {
company: AnalysisResult; company: AnalysisResult;
@@ -15,9 +14,14 @@ interface StepOutreachProps {
export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, referenceUrl, onBack, knowledgeBase }) => { export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, referenceUrl, onBack, knowledgeBase }) => {
const [fileContent, setFileContent] = useState<string>(knowledgeBase || ''); const [fileContent, setFileContent] = useState<string>(knowledgeBase || '');
const [fileName, setFileName] = useState<string>(knowledgeBase ? 'Knowledge Base from Strategy Step' : ''); const [fileName, setFileName] = useState<string>(knowledgeBase ? 'Knowledge Base from Strategy Step' : '');
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [isTranslating, setIsTranslating] = useState(false); const [isTranslating, setIsTranslating] = useState(false);
const [isGeneratingSpecific, setIsGeneratingSpecific] = useState<string | null>(null); // Track which specific role is loading
const [emails, setEmails] = useState<EmailDraft[]>([]); const [emails, setEmails] = useState<EmailDraft[]>([]);
const [availableRoles, setAvailableRoles] = useState<string[]>([]); // Suggested roles not yet generated
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@@ -45,8 +49,10 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
if (!fileContent) return; if (!fileContent) return;
setIsProcessing(true); setIsProcessing(true);
try { try {
const drafts = await generateOutreachCampaign(company, fileContent, language, referenceUrl); // Initial generation (Top 5 + Suggestions)
setEmails(drafts); const response: OutreachResponse = await generateOutreachCampaign(company, fileContent, language, referenceUrl);
setEmails(response.campaigns);
setAvailableRoles(response.available_roles || []);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
alert('Failed to generate campaign. Please try again.'); alert('Failed to generate campaign. Please try again.');
@@ -55,6 +61,28 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
} }
}; };
const handleGenerateSpecific = async (role: string) => {
if (!fileContent) return;
setIsGeneratingSpecific(role);
try {
// Generate single specific campaign
const response: OutreachResponse = await generateOutreachCampaign(company, fileContent, language, referenceUrl, role);
if (response.campaigns && response.campaigns.length > 0) {
// Add new campaign to list
setEmails(prev => [...prev, response.campaigns[0]]);
// Remove from available roles
setAvailableRoles(prev => prev.filter(r => r !== role));
// Switch view to new campaign
setActiveTab(emails.length);
}
} catch (e) {
console.error("Failed to generate specific role", e);
alert(`Failed to generate campaign for ${role}`);
} finally {
setIsGeneratingSpecific(null);
}
};
const handleTranslate = async (targetLang: Language) => { const handleTranslate = async (targetLang: Language) => {
if (emails.length === 0) return; if (emails.length === 0) return;
setIsTranslating(true); setIsTranslating(true);
@@ -103,7 +131,7 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
// Helper to render text with bold syntax **text** // Helper to render text with bold syntax **text**
const renderBoldText = (text: string) => { const renderBoldText = (text: string) => {
const parts = text.split(/(\*\*.*?\*\*)/g); const parts = text.split(/(\**.*?\**)/g);
return parts.map((part, index) => { return parts.map((part, index) => {
if (part.startsWith('**') && part.endsWith('**')) { if (part.startsWith('**') && part.endsWith('**')) {
return <strong key={index} className="font-bold text-slate-900">{part.slice(2, -2)}</strong>; return <strong key={index} className="font-bold text-slate-900">{part.slice(2, -2)}</strong>;
@@ -167,33 +195,68 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
<div className="bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden flex flex-col md:flex-row min-h-[600px]"> <div className="bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden flex flex-col md:flex-row min-h-[600px]">
{/* Sidebar Tabs */} {/* Sidebar Tabs */}
<div className="w-full md:w-72 bg-slate-50 border-r border-slate-200 p-2 flex flex-col gap-2 overflow-y-auto max-h-[600px]"> <div className="w-full md:w-80 bg-slate-50 border-r border-slate-200 flex flex-col">
{emails.map((email, idx) => ( <div className="p-4 border-b border-slate-200">
<button <h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider">Generated Campaigns</h3>
key={idx} </div>
onClick={() => setActiveTab(idx)}
className={`text-left p-3 rounded-lg text-sm font-medium transition-all group ${ {/* Generated List */}
activeTab === idx <div className="flex-1 overflow-y-auto p-2 flex flex-col gap-2">
? 'bg-white shadow-sm text-indigo-700 border border-indigo-100 ring-1 ring-indigo-500/20' {emails.map((email, idx) => (
: 'text-slate-600 hover:bg-slate-200/50' <button
}`} key={idx}
> onClick={() => setActiveTab(idx)}
<div className="flex items-center justify-between"> className={`text-left p-3 rounded-lg text-sm font-medium transition-all group ${activeTab === idx
<span className="truncate">{email.persona}</span> ? 'bg-white shadow-sm text-indigo-700 border border-indigo-100 ring-1 ring-indigo-500/20'
<span className="text-[10px] bg-slate-200 text-slate-500 px-1.5 py-0.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity">#{idx+1}</span> : 'text-slate-600 hover:bg-slate-200/50'
}`}
>
<div className="flex items-center justify-between">
<span className="truncate font-semibold">{email.persona}</span>
<span className="text-[10px] bg-slate-200 text-slate-500 px-1.5 py-0.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity">#{idx+1}</span>
</div>
<div className="text-xs text-slate-400 mt-1 truncate font-normal opacity-80">{email.subject}</div>
</button>
))}
</div>
{/* Suggestions List */}
{availableRoles.length > 0 && (
<div className="border-t border-slate-200 bg-slate-100/50 flex flex-col max-h-[40%]">
<div className="p-3 border-b border-slate-200 flex items-center gap-2">
<Sparkles size={14} className="text-indigo-500" />
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider">Other Relevant Roles</h3>
</div>
<div className="overflow-y-auto p-2 gap-2 flex flex-col">
{availableRoles.map((role, i) => (
<div key={i} className="flex items-center justify-between p-2 bg-white border border-slate-200 rounded-lg shadow-sm">
<span className="text-xs font-medium text-slate-700 truncate max-w-[140px]" title={role}>{role}</span>
<button
onClick={() => handleGenerateSpecific(role)}
disabled={!!isGeneratingSpecific}
className="bg-indigo-50 hover:bg-indigo-100 text-indigo-700 p-1.5 rounded-md transition-colors disabled:opacity-50"
title="Generate Campaign"
>
{isGeneratingSpecific === role ? (
<Loader2 size={14} className="animate-spin" />
) : (
<Plus size={14} strokeWidth={3} />
)}
</button>
</div>
))}
</div>
</div> </div>
<div className="text-xs text-slate-400 mt-1 truncate font-normal opacity-80">{email.subject}</div> )}
</button>
))}
</div> </div>
{/* Content Area */} {/* Content Area */}
<div className="flex-1 p-8 flex flex-col overflow-y-auto"> <div className="flex-1 p-8 flex flex-col overflow-y-auto bg-slate-50/30">
{activeEmail ? ( {activeEmail ? (
<> <>
<div className="mb-6"> <div className="mb-6">
<label className="block text-xs font-bold text-slate-400 uppercase tracking-wider mb-1">Subject Line</label> <label className="block text-xs font-bold text-slate-400 uppercase tracking-wider mb-1">Subject Line</label>
<div className="text-lg font-semibold text-slate-800 bg-slate-50 p-4 rounded-lg border border-slate-200 select-text"> <div className="text-lg font-semibold text-slate-800 bg-white p-4 rounded-lg border border-slate-200 select-text shadow-sm">
{activeEmail.subject} {activeEmail.subject}
</div> </div>
</div> </div>
@@ -209,14 +272,14 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
{copied ? "Copied" : "Copy Content"} {copied ? "Copied" : "Copy Content"}
</button> </button>
</div> </div>
<div className="bg-white border border-slate-200 rounded-xl p-6 text-slate-700 leading-relaxed whitespace-pre-wrap font-serif text-base select-text"> <div className="bg-white border border-slate-200 rounded-xl p-8 text-slate-700 leading-relaxed whitespace-pre-wrap font-serif text-base select-text shadow-sm">
{renderBoldText(activeEmail.body)} {renderBoldText(activeEmail.body)}
</div> </div>
</div> </div>
{/* Checklist Section */} {/* Checklist Section */}
{activeEmail.keyPoints && activeEmail.keyPoints.length > 0 && ( {activeEmail.keyPoints && activeEmail.keyPoints.length > 0 && (
<div className="border-t border-slate-100 pt-6"> <div className="border-t border-slate-200 pt-6">
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide mb-4 flex items-center gap-2"> <h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide mb-4 flex items-center gap-2">
<CheckCircle2 size={16} className="text-emerald-500" /> <CheckCircle2 size={16} className="text-emerald-500" />
Persona & KPI Analysis Persona & KPI Analysis
@@ -247,7 +310,7 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
<div className="max-w-2xl mx-auto mt-16 px-4"> <div className="max-w-2xl mx-auto mt-16 px-4">
<div className="text-center mb-10"> <div className="text-center mb-10">
<div className="inline-block p-4 bg-indigo-100 rounded-full mb-4"> <div className="inline-block p-4 bg-indigo-100 rounded-full mb-4">
<Sparkles className="text-indigo-600" size={32} /> <UserPlus className="text-indigo-600" size={32} />
</div> </div>
<h2 className="text-3xl font-bold text-slate-900 mb-4">Create Outreach Campaign</h2> <h2 className="text-3xl font-bold text-slate-900 mb-4">Create Outreach Campaign</h2>
<p className="text-lg text-slate-600"> <p className="text-lg text-slate-600">
@@ -261,7 +324,7 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
<div className="border-2 border-dashed border-slate-300 rounded-xl p-12 text-center hover:border-indigo-400 hover:bg-slate-50 transition-all"> <div className="border-2 border-dashed border-slate-300 rounded-xl p-12 text-center hover:border-indigo-400 hover:bg-slate-50 transition-all">
<input <input
type="file" type="file"
accept=".md,.txt,.markdown" accept=".md,.txt,.markdown"
onChange={handleFileUpload} onChange={handleFileUpload}
id="file-upload" id="file-upload"
className="hidden" className="hidden"
@@ -287,7 +350,7 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
<p className="text-sm text-emerald-700">{fileName}</p> <p className="text-sm text-emerald-700">{fileName}</p>
</div> </div>
<button <button
onClick={() => { setFileContent(''); setFileName(''); }} onClick={() => { setFileContent(''); setFileName(''); }}
className="text-emerald-600 hover:text-emerald-800 text-sm font-medium" className="text-emerald-600 hover:text-emerald-800 text-sm font-medium"
> >
Change Change
@@ -301,7 +364,7 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
> >
{isProcessing ? ( {isProcessing ? (
<> <>
<Loader2 className="animate-spin" /> Synthesizing Insights... <Loader2 className="animate-spin" /> Identifying Roles & Drafting Campaigns...
</> </>
) : ( ) : (
<> <>
@@ -319,4 +382,4 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
</div> </div>
</div> </div>
); );
}; };

View File

@@ -1,7 +1,4 @@
import { LeadStatus, AnalysisResult, Competitor, Language, Tier, EmailDraft, SearchStrategy, SearchSignal } from "../types"; import { LeadStatus, AnalysisResult, Competitor, Language, Tier, EmailDraft, SearchStrategy, SearchSignal, OutreachResponse } from "../types";
// const apiKey = process.env.API_KEY; // Nicht mehr direkt im Frontend verwendet
// const ai = new GoogleGenAI({ apiKey: apiKey || '' }); // Nicht mehr direkt im Frontend verwendet
// URL Konfiguration: // URL Konfiguration:
// Im Production-Build (Docker/Nginx) nutzen wir den relativen Pfad '/api', da Nginx als Reverse Proxy fungiert. // Im Production-Build (Docker/Nginx) nutzen wir den relativen Pfad '/api', da Nginx als Reverse Proxy fungiert.
@@ -19,7 +16,7 @@ const extractJson = (text: string): any => {
if (jsonMatch && jsonMatch[1]) { if (jsonMatch && jsonMatch[1]) {
try { return JSON.parse(jsonMatch[1]); } catch (e2) {} try { return JSON.parse(jsonMatch[1]); } catch (e2) {}
} }
const arrayMatch = text.match(/\[\s*[\s\S]*\s*\]/); const arrayMatch = text.match(/\\[\s\S]*\\s*\]/);
if (arrayMatch) { if (arrayMatch) {
try { return JSON.parse(arrayMatch[0]); } catch (e3) {} try { return JSON.parse(arrayMatch[0]); } catch (e3) {}
} }
@@ -129,7 +126,7 @@ export const analyzeCompanyWithStrategy = async (
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
companyName, companyName,
strategy, strategy,
targetMarket: language === 'de' ? 'Germany' : 'USA' // Einfache Ableitung, kann verfeinert werden targetMarket: language === 'de' ? 'Germany' : 'USA' // Einfache Ableitung, kann verfeinert werden
@@ -175,9 +172,10 @@ export const generateOutreachCampaign = async (
companyData: AnalysisResult, companyData: AnalysisResult,
knowledgeBase: string, knowledgeBase: string,
language: Language, language: Language,
referenceUrl: string referenceUrl: string,
): Promise<EmailDraft[]> => { specificRole?: string
console.log(`Frontend: Starte Outreach-Generierung für ${companyData.companyName}...`); ): Promise<OutreachResponse> => {
console.log(`Frontend: Starte Outreach-Generierung für ${companyData.companyName} (Role: ${specificRole || "All"})...`);
try { try {
const response = await fetch(`${API_BASE_URL}/generate-outreach`, { const response = await fetch(`${API_BASE_URL}/generate-outreach`, {
@@ -185,10 +183,11 @@ export const generateOutreachCampaign = async (
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
companyData, companyData,
knowledgeBase, knowledgeBase,
referenceUrl referenceUrl,
specific_role: specificRole
}), }),
}); });
@@ -197,45 +196,57 @@ export const generateOutreachCampaign = async (
throw new Error(`Backend-Fehler: ${errorData.error || response.statusText}`); throw new Error(`Backend-Fehler: ${errorData.error || response.statusText}`);
} }
const result = await response.json(); const result = await response.json(); // Expected: { campaigns: [...], available_roles: [...] } or single object
console.log(`Frontend: Outreach-Generierung für ${companyData.companyName} erfolgreich.`); console.log(`Frontend: Outreach-Generierung erfolgreich.`, result);
// Transform new backend structure to match frontend EmailDraft interface let rawCampaigns: any[] = [];
if (Array.isArray(result)) { let availableRoles: string[] = [];
return result.map((item: any) => {
// Construct a body that shows the sequence
let fullBody = "";
const firstSubject = item.emails?.[0]?.subject || "No Subject";
if (item.emails && Array.isArray(item.emails)) {
item.emails.forEach((mail: any, idx: number) => {
fullBody += `### Email ${idx + 1}: ${mail.subject}\n\n`;
fullBody += `${mail.body}\n\n`;
if (idx < item.emails.length - 1) fullBody += `\n---\n\n`;
});
} else {
// Fallback for flat structure or error
fullBody = item.body || "No content generated.";
}
return { // Handle Mode A (Batch) vs Mode B (Single) response structures
persona: item.target_role || "Unknown Role", if (result.campaigns && Array.isArray(result.campaigns)) {
subject: firstSubject, rawCampaigns = result.campaigns;
body: fullBody, availableRoles = result.available_roles || [];
keyPoints: item.rationale ? [item.rationale] : [] } else if (result.target_role && result.emails) {
}; // Single campaign response (Mode B)
}); rawCampaigns = [result];
} else if (result.campaign && Array.isArray(result.campaign)) { } else if (Array.isArray(result)) {
return result.campaign as EmailDraft[]; // Legacy fallback
rawCampaigns = result;
} }
return []; const processedCampaigns: EmailDraft[] = rawCampaigns.map((item: any) => {
let fullBody = "";
const firstSubject = item.emails?.[0]?.subject || "No Subject";
if (item.emails && Array.isArray(item.emails)) {
item.emails.forEach((mail: any, idx: number) => {
fullBody += `### Email ${idx + 1}: ${mail.subject}\n\n`;
fullBody += `${mail.body}\n\n`;
if (idx < item.emails.length - 1) fullBody += `\n---\n\n`;
});
} else {
fullBody = item.body || "No content generated.";
}
return {
persona: item.target_role || "Unknown Role",
subject: firstSubject,
body: fullBody,
keyPoints: item.rationale ? [item.rationale] : []
};
});
return {
campaigns: processedCampaigns,
available_roles: availableRoles
};
} catch (error) { } catch (error) {
console.error(`Frontend: Outreach-Generierung fehlgeschlagen für ${companyData.companyName}`, error); console.error(`Frontend: Outreach-Generierung fehlgeschlagen für ${companyData.companyName}`, error);
throw error; throw error;
} }
}; };
export const translateEmailDrafts = async (drafts: EmailDraft[], targetLanguage: Language): Promise<EmailDraft[]> => { export const translateEmailDrafts = async (drafts: EmailDraft[], targetLanguage: Language): Promise<EmailDraft[]> => {
// Dieser Teil muss noch im Python-Backend oder direkt im Frontend implementiert werden // Dieser Teil muss noch im Python-Backend oder direkt im Frontend implementiert werden
console.warn("translateEmailDrafts ist noch nicht im Python-Backend implementiert."); console.warn("translateEmailDrafts ist noch nicht im Python-Backend implementiert.");

View File

@@ -89,3 +89,8 @@ export interface EmailDraft {
body: string; body: string;
keyPoints: string[]; keyPoints: string[];
} }
export interface OutreachResponse {
campaigns: EmailDraft[];
available_roles: string[];
}

View File

@@ -544,25 +544,64 @@ def analyze_company(company_name, strategy, target_market):
"dataSource": "Error" "dataSource": "Error"
} }
def generate_outreach_campaign(company_data_json, knowledge_base_content, reference_url): def generate_outreach_campaign(company_data_json, knowledge_base_content, reference_url, specific_role=None):
""" """
Erstellt personalisierte E-Mail-Kampagnen basierend auf Audit-Daten und einer strukturierten Wissensdatenbank. Erstellt personalisierte E-Mail-Kampagnen.
Generiert spezifische Ansprachen für verschiedene Rollen (Personas). Modus A (Default): Generiert Top 5 Kampagnen + Liste weiterer relevanter Rollen.
Modus B (specific_role): Generiert nur Kampagne für diese eine Rolle.
""" """
company_name = company_data_json.get('companyName', 'Unknown') company_name = company_data_json.get('companyName', 'Unknown')
logger.info(f"--- STARTING ROLE-BASED OUTREACH GENERATION FOR: {company_name} ---") logger.info(f"--- STARTING OUTREACH GENERATION FOR: {company_name} (Role: {specific_role if specific_role else 'Top 5'}) ---")
api_key = load_gemini_api_key() api_key = load_gemini_api_key()
# Switch to stable 2.5-pro model
GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={api_key}" GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={api_key}"
if specific_role:
# --- MODE B: SINGLE ROLE GENERATION ---
task_description = f"""
--- TASK ---
1. **Focus**: Create a highly specific 3-step email campaign ONLY for the role: '{specific_role}'.
2. **Analyze**: Use the Audit Facts to find specific hooks for this role.
3. **Draft**: Write the sequence (Opening, Follow-up, Break-up).
"""
output_format = """
--- OUTPUT FORMAT (Strictly JSON) ---
{
"target_role": "The requested role",
"rationale": "Why this fits...",
"emails": [ ... ]
}
"""
else:
# --- MODE A: INITIAL BATCH (TOP 5 + SUGGESTIONS) ---
task_description = f"""
--- TASK ---
1. **Analyze**: Match the Target Company (Input 2) to the most relevant 'Zielbranche/Segment' from the Knowledge Base (Input 1).
2. **Identify Roles**: Identify ALL relevant 'Rollen' (Personas) from the Knowledge Base that fit this company.
3. **Select Top 5**: Choose the 5 most promising roles for immediate outreach based on the Audit findings.
4. **Draft Campaigns**: For EACH of the Top 5 roles, write a 3-step email sequence.
5. **List Others**: List the names of the other relevant roles that you identified but did NOT generate campaigns for yet.
"""
output_format = """
--- OUTPUT FORMAT (Strictly JSON) ---
{
"campaigns": [
{
"target_role": "Role Name",
"rationale": "Why selected...",
"emails": [ ... ]
},
... (Top 5)
],
"available_roles": [ "Role 6", "Role 7", ... ]
}
"""
prompt = f""" prompt = f"""
You are a Strategic Key Account Manager and deeply technical Industry Insider. You are a Strategic Key Account Manager and deeply technical Industry Insider.
Your goal is to write highly personalized, **operationally specific** outreach emails to the company '{company_name}'. Your goal is to write highly personalized, **operationally specific** outreach emails to the company '{company_name}'.
--- INPUT 1: YOUR IDENTITY & STRATEGY (The Sender) --- --- INPUT 1: YOUR IDENTITY & STRATEGY (The Sender) ---
The following Markdown contains your company's identity, products, and strategy.
You act as the sales representative for the company described here:
{knowledge_base_content} {knowledge_base_content}
--- INPUT 2: THE TARGET COMPANY (Audit Facts) --- --- INPUT 2: THE TARGET COMPANY (Audit Facts) ---
@@ -571,52 +610,21 @@ def generate_outreach_campaign(company_data_json, knowledge_base_content, refere
--- INPUT 3: THE REFERENCE CLIENT (Social Proof) --- --- INPUT 3: THE REFERENCE CLIENT (Social Proof) ---
Reference Client URL: {reference_url} Reference Client URL: {reference_url}
CRITICAL: This 'Reference Client' is an existing happy customer of ours. They are the "Seed Company" used to find the Target Company (Lookalike). CRITICAL: This 'Reference Client' is an existing happy customer of ours. You MUST mention them by name to establish trust.
You MUST mention this Reference Client by name (derive it from the URL, e.g., 'schindler.com' -> 'Schindler') to establish trust.
--- TASK --- {task_description}
1. **Analyze**: Match the Target Company (Input 2) to the most relevant 'Zielbranche/Segment' from the Knowledge Base (Input 1).
2. **Select Roles**: Identify **up to 5** of the most distinct and relevant 'Rollen' (Personas) from the Knowledge Base for this specific company situation.
- Prioritize roles where the audit findings (e.g., specific competitor tech, growth pains) offer the strongest hook.
- *Example:* If the audit says they use a competitor (risk of lock-in), select a role like "Strategic Purchaser" or "Head of R&D".
3. **Draft Campaigns**: For **EACH** of the selected roles, write a 3-step email sequence.
--- TONE & STYLE GUIDELINES (CRITICAL) --- --- TONE & STYLE GUIDELINES (CRITICAL) ---
- **Perspective:** Operational Expert & Insider. NOT generic marketing. - **Perspective:** Operational Expert & Insider. NOT generic marketing.
- **Be Gritty & Specific:** Do NOT use fluff like "optimize efficiency" or "streamline processes" without context. - **Be Gritty & Specific:** Use hard, operational keywords from the Knowledge Base (e.g., "ASNs", "8D-Reports").
- Use **hard, operational keywords** from the Knowledge Base (e.g., "ASNs", "VMI", "8D-Reports", "Maverick Buying", "Bandstillstand", "Sonderfahrten", "PPAP").
- Show you understand their daily pain.
- **Narrative Arc:** - **Narrative Arc:**
1. "I noticed [Fact from Audit/Tech Stack]..." (e.g., "You rely on PDF orders via Jaggaer...") 1. "I noticed [Fact from Audit]..."
2. "In [Industry], this often leads to [Operational Pain]..." (e.g., "missing ASNs causing delays at the hub.") 2. "In [Industry], this often leads to [Pain]..."
3. "We helped [Reference Client Name] solve exactly this by [Specific Solution]..." 3. "We helped [Reference Client] solve this..."
4. "Let's discuss how to get [Operational Gain] without replacing your ERP." 4. "Let's discuss [Gain]."
- **Mandatory Social Proof:** You MUST mention the Reference Client Name (from Input 3) in the email body or footer. - **Language:** German.
- **Language:** German (as the inputs are German).
--- OUTPUT FORMAT (Strictly JSON) --- {output_format}
Returns a list of campaigns.
[
{{
"target_role": "Name of the Role (e.g. Leiter F&E)",
"rationale": "Why this role? (e.g. Because the audit found dependency on Competitor X...)",
"emails": [
{{
"subject": "Specific Subject Line",
"body": "Email Body..."
}},
{{
"subject": "Re: Subject",
"body": "Follow-up Body..."
}},
{{
"subject": "Final Check",
"body": "Final Body..."
}}
]
}},
... (Second Role)
]
""" """
payload = { payload = {
@@ -626,12 +634,10 @@ def generate_outreach_campaign(company_data_json, knowledge_base_content, refere
try: try:
logger.info("Sende Campaign-Anfrage an Gemini API...") logger.info("Sende Campaign-Anfrage an Gemini API...")
# logger.debug(f"Rohe Gemini API-Anfrage (JSON): {json.dumps(payload, indent=2)}")
response = requests.post(GEMINI_API_URL, json=payload, headers={'Content-Type': 'application/json'}) response = requests.post(GEMINI_API_URL, json=payload, headers={'Content-Type': 'application/json'})
response.raise_for_status() response.raise_for_status()
response_data = response.json() response_data = response.json()
logger.info(f"Gemini API-Antwort erhalten (Status: {response.status_code}).") logger.info(f"Gemini API-Antwort erhalten (Status: {response.status_code}).")
# logger.debug(f"Rohe API-Antwort (JSON): {json.dumps(response_data, indent=2)}")
text = response_data['candidates'][0]['content']['parts'][0]['text'] text = response_data['candidates'][0]['content']['parts'][0]['text']
result = _extract_json_from_text(text) result = _extract_json_from_text(text)
@@ -642,7 +648,7 @@ def generate_outreach_campaign(company_data_json, knowledge_base_content, refere
return result return result
except Exception as e: except Exception as e:
logger.error(f"Campaign generation failed for {company_name}: {e}") logger.error(f"Campaign generation failed for {company_name}: {e}")
return [{"error": str(e)}] return {"error": str(e)}
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@@ -653,7 +659,8 @@ def main():
parser.add_argument("--company_name") parser.add_argument("--company_name")
parser.add_argument("--strategy_json") parser.add_argument("--strategy_json")
parser.add_argument("--summary_of_offer") parser.add_argument("--summary_of_offer")
parser.add_argument("--company_data_file") # For generate_outreach parser.add_argument("--company_data_file")
parser.add_argument("--specific_role") # New argument
args = parser.parse_args() args = parser.parse_args()
if args.mode == "generate_strategy": if args.mode == "generate_strategy":
@@ -671,7 +678,7 @@ def main():
elif args.mode == "generate_outreach": elif args.mode == "generate_outreach":
with open(args.company_data_file, "r") as f: company_data = json.load(f) with open(args.company_data_file, "r") as f: company_data = json.load(f)
with open(args.context_file, "r") as f: knowledge_base = f.read() with open(args.context_file, "r") as f: knowledge_base = f.read()
print(json.dumps(generate_outreach_campaign(company_data, knowledge_base, args.reference_url))) print(json.dumps(generate_outreach_campaign(company_data, knowledge_base, args.reference_url, args.specific_role)))
if __name__ == "__main__": if __name__ == "__main__":