feat(market-intel): implement role-based campaign engine and gritty reporting

- Implementierung der rollenbasierten Campaign-Engine mit operativem Fokus (Grit).
- Integration von Social Proof (Referenzkunden) in die E-Mail-Generierung.
- Erweiterung des Deep Tech Audits um gezielte Wettbewerber-Recherche (Technographic Search).
- Fix des Lösch-Bugs in der Target-Liste und Optimierung des Frontend-States.
- Erweiterung des Markdown-Exports um transparente Proof-Links und Evidenz.
- Aktualisierung der Dokumentation in readme.md und market_intel_backend_plan.md.
This commit is contained in:
2025-12-22 15:54:06 +00:00
parent e4aad40344
commit a077c48573
9 changed files with 545 additions and 76 deletions

View File

@@ -222,6 +222,7 @@ const App: React.FC = () => {
language={language} language={language}
referenceUrl={referenceUrl} referenceUrl={referenceUrl}
onBack={handleBack} onBack={handleBack}
knowledgeBase={productContext}
/> />
)} )}
</main> </main>

View File

@@ -9,17 +9,26 @@ interface StepOutreachProps {
language: Language; language: Language;
referenceUrl: string; referenceUrl: string;
onBack: () => void; onBack: () => void;
knowledgeBase?: string; // New prop for pre-loaded context
} }
export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, referenceUrl, onBack }) => { export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, referenceUrl, onBack, knowledgeBase }) => {
const [fileContent, setFileContent] = useState<string>(''); const [fileContent, setFileContent] = useState<string>(knowledgeBase || '');
const [fileName, setFileName] = useState<string>(''); 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 [emails, setEmails] = useState<EmailDraft[]>([]); const [emails, setEmails] = useState<EmailDraft[]>([]);
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
// If knowledgeBase prop changes, update state (useful if it loads late)
React.useEffect(() => {
if (knowledgeBase && !fileContent) {
setFileContent(knowledgeBase);
setFileName('Knowledge Base from Strategy Step');
}
}, [knowledgeBase]);
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {

View File

@@ -46,8 +46,26 @@ export const StepReport: React.FC<StepReportProps> = ({ results, strategy, onRes
const headers = ["Company", "Prio", "Rev/Emp", "Status", ...signalHeaders, "Recommendation"]; const headers = ["Company", "Prio", "Rev/Emp", "Status", ...signalHeaders, "Recommendation"];
const rows = sortedResults.map(r => { const rows = sortedResults.map(r => {
const signalValues = strategy.signals.map(s => r.dynamicAnalysis[s.id]?.value || '-'); const signalValues = strategy.signals.map(s => {
return `| ${r.companyName} | ${r.tier} | ${r.revenue} / ${r.employees} | ${r.status} | ${signalValues.join(" | ")} | ${r.recommendation} |`; const data = r.dynamicAnalysis[s.id];
if (!data) return '-';
let content = data.value || '-';
// Sanitize content pipes
content = content.replace(/\|/g, '\\|');
if (data.proof) {
// Sanitize proof pipes and newlines
const safeProof = data.proof.replace(/\|/g, '\\|').replace(/(\r\n|\n|\r)/gm, ' ');
content += `<br><sub>*Proof: ${safeProof}*</sub>`;
}
return content;
});
// Helper to sanitize other fields
const safe = (str: string) => (str || '').replace(/\|/g, '\\|').replace(/(\r\n|\n|\r)/gm, ' ');
return `| ${safe(r.companyName)} | ${r.tier} | ${safe(r.revenue)} / ${safe(r.employees)} | ${r.status} | ${signalValues.join(" | ")} | ${safe(r.recommendation)} |`;
}); });
const content = ` const content = `

View File

@@ -30,14 +30,18 @@ export const StepReview: React.FC<StepReviewProps> = ({ competitors, categorized
}; };
const renderCompetitorList = (comps: Competitor[], category: string) => { const renderCompetitorList = (comps: Competitor[], category: string) => {
if (!comps || comps.length === 0) { // Filter out competitors that have been removed from the main list
const activeIds = new Set(competitors.map(c => c.id));
const activeComps = comps.filter(c => activeIds.has(c.id));
if (!activeComps || activeComps.length === 0) {
return ( return (
<li className="p-4 text-center text-slate-500 italic bg-white rounded-md border border-slate-100 mb-2 last:mb-0"> <li className="p-4 text-center text-slate-500 italic bg-white rounded-md border border-slate-100 mb-2 last:mb-0">
Keine {category} Konkurrenten gefunden. Keine {category} Konkurrenten gefunden.
</li> </li>
); );
} }
return comps.map((comp) => ( return activeComps.map((comp) => (
<li key={comp.id} className="flex items-start justify-between p-4 hover:bg-slate-50 transition-colors group bg-white rounded-md border border-slate-100 mb-2 last:mb-0"> <li key={comp.id} className="flex items-start justify-between p-4 hover:bg-slate-50 transition-colors group bg-white rounded-md border border-slate-100 mb-2 last:mb-0">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -269,6 +269,89 @@ app.post('/api/analyze-company', async (req, res) => {
} }
}); });
// API-Endpunkt für generate-outreach
app.post('/api/generate-outreach', async (req, res) => {
console.log(`[${new Date().toISOString()}] HIT: /api/generate-outreach`);
const { companyData, knowledgeBase, referenceUrl } = req.body;
if (!companyData || !knowledgeBase) {
console.error('Validation Error: Missing companyData or knowledgeBase for generate-outreach.');
return res.status(400).json({ error: 'Missing companyData or knowledgeBase' });
}
const tempDataFilePath = path.join(__dirname, 'tmp', `outreach_data_${Date.now()}.json`);
const tempContextFilePath = path.join(__dirname, 'tmp', `outreach_context_${Date.now()}.md`);
const tmpDir = path.join(__dirname, 'tmp');
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir);
}
try {
fs.writeFileSync(tempDataFilePath, JSON.stringify(companyData));
fs.writeFileSync(tempContextFilePath, knowledgeBase);
console.log(`Successfully wrote temporary files for outreach.`);
const pythonExecutable = path.join(__dirname, '..', '.venv', 'bin', 'python3');
const pythonScript = path.join(__dirname, '..', 'market_intel_orchestrator.py');
const scriptArgs = [
pythonScript,
'--mode', 'generate_outreach',
'--company_data_file', tempDataFilePath,
'--context_file', tempContextFilePath,
'--reference_url', referenceUrl || ''
];
console.log(`Spawning Outreach Generation for ${companyData.companyName}...`);
const pythonProcess = spawn(pythonExecutable, scriptArgs, {
env: { ...process.env, PYTHONPATH: path.join(__dirname, '..', '.venv', 'lib', 'python3.11', 'site-packages') }
});
let pythonOutput = '';
let pythonError = '';
pythonProcess.stdout.on('data', (data) => {
pythonOutput += data.toString();
});
pythonProcess.stderr.on('data', (data) => {
pythonError += data.toString();
});
pythonProcess.on('close', (code) => {
console.log(`Outreach Generation finished with exit code: ${code}`);
// Clean up
if (fs.existsSync(tempDataFilePath)) fs.unlinkSync(tempDataFilePath);
if (fs.existsSync(tempContextFilePath)) fs.unlinkSync(tempContextFilePath);
if (code !== 0) {
console.error(`Python script (generate_outreach) exited with error.`);
return res.status(500).json({ error: 'Python script failed', details: pythonError });
}
try {
const result = JSON.parse(pythonOutput);
res.json(result);
} catch (parseError) {
console.error('Failed to parse Python output (generate_outreach) as JSON:', parseError);
res.status(500).json({ error: 'Invalid JSON from Python script', rawOutput: pythonOutput, details: pythonError });
}
});
pythonProcess.on('error', (err) => {
console.error(`FATAL: Failed to start python process for outreach.`, err);
if (fs.existsSync(tempDataFilePath)) fs.unlinkSync(tempDataFilePath);
if (fs.existsSync(tempContextFilePath)) fs.unlinkSync(tempContextFilePath);
res.status(500).json({ error: 'Failed to start Python process', details: err.message });
});
} catch (err) {
console.error(`Internal Server Error in /api/generate-outreach: ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// Start des Servers // Start des Servers
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Node.js API Bridge running on http://localhost:${PORT}`); console.log(`Node.js API Bridge running on http://localhost:${PORT}`);

View File

@@ -170,11 +170,65 @@ export const generateOutreachCampaign = async (
language: Language, language: Language,
referenceUrl: string referenceUrl: string
): Promise<EmailDraft[]> => { ): Promise<EmailDraft[]> => {
// Dieser Teil muss noch im Python-Backend implementiert werden console.log(`Frontend: Starte Outreach-Generierung für ${companyData.companyName}...`);
console.warn("generateOutreachCampaign ist noch nicht im Python-Backend implementiert.");
return []; try {
}; const response = await fetch(`${API_BASE_URL}/generate-outreach`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
companyData,
knowledgeBase,
referenceUrl
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Backend-Fehler: ${errorData.error || response.statusText}`);
}
const result = await response.json();
console.log(`Frontend: Outreach-Generierung für ${companyData.companyName} erfolgreich.`);
// Transform new backend structure to match frontend EmailDraft interface
if (Array.isArray(result)) {
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 {
persona: item.target_role || "Unknown Role",
subject: firstSubject,
body: fullBody,
keyPoints: item.rationale ? [item.rationale] : []
};
});
} else if (result.campaign && Array.isArray(result.campaign)) {
return result.campaign as EmailDraft[];
}
return [];
} catch (error) {
console.error(`Frontend: Outreach-Generierung fehlgeschlagen für ${companyData.companyName}`, 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

@@ -46,6 +46,7 @@ Die Logik aus `geminiService.ts` wird in Python-Funktionen innerhalb von `market
--- ---
### Funktion 2: `identify_competitors` ### Funktion 2: `identify_competitors`
- **Trigger:** Aufruf mit `--mode identify_competitors`. - **Trigger:** Aufruf mit `--mode identify_competitors`.
@@ -56,6 +57,7 @@ Die Logik aus `geminiService.ts` wird in Python-Funktionen innerhalb von `market
--- ---
### Funktion 3: `run_full_analysis` ### Funktion 3: `run_full_analysis`
- **Trigger:** Aufruf mit `--mode run_analysis`. - **Trigger:** Aufruf mit `--mode run_analysis`.
@@ -70,6 +72,7 @@ Die Logik aus `geminiService.ts` wird in Python-Funktionen innerhalb von `market
--- ---
### Funktion 4: `generate_outreach_campaign` ### Funktion 4: `generate_outreach_campaign`
- **Trigger:** Aufruf mit `--mode generate_outreach`. - **Trigger:** Aufruf mit `--mode generate_outreach`.
@@ -105,8 +108,25 @@ Wir haben heute das gesamte System von einer instabilen n8n-Abhängigkeit zu ein
- **Frontend-Abstürze:** Absicherung des Reports gegen fehlende Datenpunkte. - **Frontend-Abstürze:** Absicherung des Reports gegen fehlende Datenpunkte.
--- ---
### Nächste Ziele für die nächste Sitzung:
1. **Schritt 4: Hyper-personalisierte Campaign-Generation:** Implementierung der Funktion, die basierend auf den Audit-Fakten (z.B. gefundene Software-Stacks oder Nachhaltigkeits-Ziele) maßgeschneiderte E-Mails erstellt.
2. **Stabilitäts-Check:** Testen des Batch-Audits mit einer größeren Anzahl an Firmen (Timeout/Rate-Limit Handling).
3. **Report-Polishing:** Integration der "Proof-Links" direkt in die MD-Export-Funktion.
## 6. Status Update (2025-12-22) - Campaign Engine & Reporting
### Erreichte Meilensteine:
1. **Rollenbasierte Campaign-Engine:**
* Die Funktion `generate_outreach_campaign` wurde komplett überarbeitet.
* Sie nutzt nun die volle Tiefe der Knowledge Base (`yamaichi_neu.md`), um **personalisierte Sequenzen für spezifische Rollen** (z.B. "Hardware-Entwickler" vs. "Einkäufer") zu erstellen.
* Die Ansprache erfolgt strikt im "Partner auf Augenhöhe"-Tonfall.
* **Social Proof Integration:** Der Absender (`reference_url`) wird als Beweis der Kompetenz inkl. passender KPIs im Abbinder integriert.
* **"Grit"-Prompting:** Der Prompt wurde massiv geschärft, um operative Schmerzpunkte ("ASNs", "Bandstillstand") statt Marketing-Bla-Bla zu nutzen.
2. **Report Polishing (Frontend):**
* Der Markdown-Export (`StepReport.tsx`) wurde erweitert.
* Er enthält nun die **"Proof-Links"** (Beweise/URLs) direkt in den Tabellenzellen, sauber formatiert. Damit ist die Herleitung der Ergebnisse (z.B. "Warum nutzt der Kunde Ariba?") auch im Export transparent nachvollziehbar.
3. **Frontend UX & Bugfixes:**
* **Kein doppelter Upload:** `StepOutreach.tsx` wurde angepasst, um den Strategie-Kontext aus Schritt 1 direkt zu übernehmen.
* **Lösch-Bug:** `StepReview.tsx` wurde korrigiert, sodass gelöschte Unternehmen sofort aus der UI verschwinden.
* **Crash-Fix:** Die Behandlung der API-Antwort in `geminiService.ts` wurde gehärtet, um die neue verschachtelte Antwortstruktur der Campaign-Engine korrekt zu verarbeiten.
### Nächste Schritte:
* **Stabilitäts-Test:** Ausführung eines Batch-Audits mit >20 Firmen, um Rate-Limits und Fehlerbehandlung unter Last zu prüfen.

View File

@@ -68,7 +68,9 @@ def get_website_text(url):
for tag in soup(['script', 'style', 'nav', 'footer', 'header']): for tag in soup(['script', 'style', 'nav', 'footer', 'header']):
tag.decompose() tag.decompose()
text = soup.get_text(separator=' ', strip=True) text = soup.get_text(separator=' ', strip=True)
return text[:15000] # Erhöhtes Limit für besseren Kontext # Bereinigung des Textes von nicht-druckbaren Zeichen
text = re.sub(r'[^\x20-\x7E\n\r\t]', '', text)
return text[:10000] # Limit für besseren Kontext
except Exception as e: except Exception as e:
logger.error(f"Scraping failed for {url}: {e}") logger.error(f"Scraping failed for {url}: {e}")
return None return None
@@ -109,8 +111,14 @@ def serp_search(query, num_results=3):
def _extract_target_industries_from_context(context_content): def _extract_target_industries_from_context(context_content):
md = context_content md = context_content
# Versuche verschiedene Muster für die Tabelle, falls das Format variiert
step2_match = re.search(r'##\s*Schritt\s*2:[\s\S]*?(?=\n##\s*Schritt\s*\d:|\s*$)', md, re.IGNORECASE) step2_match = re.search(r'##\s*Schritt\s*2:[\s\S]*?(?=\n##\s*Schritt\s*\d:|\s*$)', md, re.IGNORECASE)
if not step2_match: return [] if not step2_match:
# Fallback: Suche nach "Zielbranche" irgendwo im Text
match = re.search(r'Zielbranche\s*\|?\s*([^|\n]+)', md, re.IGNORECASE)
if match:
return [s.strip() for s in match.group(1).split(',')]
return []
table_lines = [] table_lines = []
in_table = False in_table = False
@@ -132,13 +140,37 @@ def _extract_target_industries_from_context(context_content):
if len(cells) > col_idx: industries.append(cells[col_idx]) if len(cells) > col_idx: industries.append(cells[col_idx])
return list(set(industries)) return list(set(industries))
def _extract_json_from_text(text):
"""
Versucht, ein JSON-Objekt aus einem Textstring zu extrahieren,
unabhängig von Markdown-Formatierung (```json ... ```).
"""
try:
# 1. Versuch: Direktersatz von Markdown-Tags (falls vorhanden)
clean_text = text.replace("```json", "").replace("```", "").strip()
return json.loads(clean_text)
except json.JSONDecodeError:
pass
try:
# 2. Versuch: Regex Suche nach dem ersten { und letzten }
json_match = re.search(r"(\{[\s\S]*\})", text)
if json_match:
return json.loads(json_match.group(1))
except json.JSONDecodeError:
pass
logger.error(f"JSON Parsing fehlgeschlagen. Roher Text: {text[:500]}...")
return None
def generate_search_strategy(reference_url, context_content): def generate_search_strategy(reference_url, context_content):
logger.info(f"Generating strategy for {reference_url}") logger.info(f"Generating strategy for {reference_url}")
api_key = load_gemini_api_key() api_key = load_gemini_api_key()
target_industries = _extract_target_industries_from_context(context_content) target_industries = _extract_target_industries_from_context(context_content)
homepage_text = get_website_text(reference_url) homepage_text = get_website_text(reference_url)
GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1/models/gemini-2.5-pro:generateContent?key={api_key}" # Switch to stable 2.5-pro model (which works for v1beta)
GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={api_key}"
prompt = f""" prompt = f"""
You are a B2B Market Intelligence Architect. You are a B2B Market Intelligence Architect.
@@ -150,19 +182,30 @@ def generate_search_strategy(reference_url, context_content):
{', '.join(target_industries)} {', '.join(target_industries)}
--- REFERENCE CLIENT HOMEPAGE --- --- REFERENCE CLIENT HOMEPAGE ---
{homepage_text} {homepage_text[:10000] if homepage_text else "No Homepage Text"}
TASK: --- TASK ---
1. Create a 1-sentence 'summaryOfOffer'. Based on the context and the reference client's homepage, develop a search strategy to find similar companies (competitors/lookalikes) and audit them to find sales triggers.
2. Define an 'idealCustomerProfile' based on the reference client.
3. Identify 3-5 'signals'.
FOR EACH SIGNAL, you MUST define a 'proofStrategy':
- 'likelySource': Where to find the proof (e.g., "Datenschutz", "Jobs", "Case Studies", "Homepage", "Press").
- 'searchQueryTemplate': A specific Google search query template to find this proof. Use '{{COMPANY}}' as placeholder for the company name.
Example: "site:{{COMPANY}} 'it-leiter' sap" or "{{COMPANY}} nachhaltigkeitsbericht 2024 filetype:pdf".
STRICTLY output only valid JSON: 1. **summaryOfOffer**: A 1-sentence summary of what the reference client sells.
2. **idealCustomerProfile**: A concise definition of the Ideal Customer Profile (ICP) based on the reference client.
3. **signals**: Identify exactly 4 specific digital signals.
- **CRITICAL**: One signal MUST be "Technographic / Incumbent Search". It must look for existing competitor software or legacy systems that our offer replaces or complements (e.g., "Uses SAP Ariba", "Has Supplier Portal", "Uses Salesforce").
- The other 3 signals should focus on business pains or strategic fit (e.g., "Sustainability Report", "Supply Chain Complexity").
--- SIGNAL DEFINITION ---
For EACH signal, you MUST provide:
- `id`: A unique ID (e.g., "sig_1").
- `name`: A short, descriptive name.
- `description`: What does this signal indicate?
- `targetPageKeywords`: A list of 3-5 keywords to look for on a company's website (e.g., ["career", "jobs"] for a hiring signal).
- `proofStrategy`: An object containing:
- `likelySource`: Where on the website or web is this info found? (e.g., "Careers Page").
- `searchQueryTemplate`: A Google search query to find this. Use `{{COMPANY}}` as a placeholder for the company name.
Example: `site:{{COMPANY}} "software engineer" OR "developer"`
--- OUTPUT FORMAT ---
Return ONLY a valid JSON object.
{{ {{
"summaryOfOffer": "...", "summaryOfOffer": "...",
"idealCustomerProfile": "...", "idealCustomerProfile": "...",
@@ -171,59 +214,103 @@ def generate_search_strategy(reference_url, context_content):
"id": "sig_1", "id": "sig_1",
"name": "...", "name": "...",
"description": "...", "description": "...",
"targetPageKeywords": ["homepage"], "targetPageKeywords": ["..."],
"proofStrategy": {{ "proofStrategy": {{
"likelySource": "...", "likelySource": "...",
"searchQueryTemplate": "..." "searchQueryTemplate": "..."
}} }}
}} }},
...
] ]
}} }}
""" """
payload = {"contents": [{"parts": [{"text": prompt}]}]} payload = {"contents": [{"parts": [{"text": prompt}]}]}
logger.info("Sende Anfrage an Gemini API...")
# logger.debug(f"Rohe Gemini API-Anfrage (JSON): {json.dumps(payload, indent=2)}")
try: try:
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()
res_json = response.json() res_json = response.json()
logger.info(f"Gemini API-Antwort erhalten (Status: {response.status_code}).")
text = res_json['candidates'][0]['content']['parts'][0]['text'] text = res_json['candidates'][0]['content']['parts'][0]['text']
if "```json" in text: text = text.split("```json")[1].split("```")[0].strip() result = _extract_json_from_text(text)
return json.loads(text)
if not result:
raise ValueError("Konnte kein valides JSON extrahieren")
return result
except Exception as e: except Exception as e:
logger.error(f"Strategy generation failed: {e}") logger.error(f"Strategy generation failed: {e}")
return {"error": str(e)} # Return fallback to avoid frontend crash
return {
"summaryOfOffer": "Error generating strategy. Please check logs.",
"idealCustomerProfile": "Error generating ICP. Please check logs.",
"signals": []
}
def identify_competitors(reference_url, target_market, industries, summary_of_offer=None): def identify_competitors(reference_url, target_market, industries, summary_of_offer=None):
logger.info(f"Identifying competitors for {reference_url}") logger.info(f"Identifying competitors for {reference_url}")
api_key = load_gemini_api_key() api_key = load_gemini_api_key()
GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1/models/gemini-2.5-pro:generateContent?key={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}"
prompt = f""" prompt = f"""
Find 3-5 competitors/lookalikes for the company at {reference_url}. You are a B2B Market Analyst. Find 3-5 direct competitors or highly similar companies (lookalikes) for the company at `{reference_url}`.
Offer context: {summary_of_offer}
Target Market: {target_market}
Industries: {', '.join(industries)}
Categorize into 'localCompetitors', 'nationalCompetitors', 'internationalCompetitors'. --- CONTEXT ---
Return ONLY JSON. - Offer: {summary_of_offer}
- Target Market: {target_market}
- Relevant Industries: {', '.join(industries)}
--- TASK ---
Identify competitors and categorize them into three groups:
1. 'localCompetitors': Competitors in the same immediate region/city.
2. 'nationalCompetitors': Competitors operating across the same country.
3. 'internationalCompetitors': Global players.
For EACH competitor, you MUST provide:
- `id`: A unique, URL-friendly identifier (e.g., "competitor-name-gmbh").
- `name`: The official, full name of the company.
- `description`: A concise explanation of why they are a competitor.
--- OUTPUT FORMAT ---
Return ONLY a valid JSON object with the following structure:
{{
"localCompetitors": [ {{ "id": "...", "name": "...", "description": "..." }} ],
"nationalCompetitors": [ ... ],
"internationalCompetitors": [ ... ]
}}
""" """
payload = {"contents": [{"parts": [{"text": prompt}]}]} payload = {"contents": [{"parts": [{"text": prompt}]}]}
logger.info("Sende Anfrage an Gemini API...")
# logger.debug(f"Rohe Gemini API-Anfrage (JSON): {json.dumps(payload, indent=2)}")
try: try:
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()
res_json = response.json() res_json = response.json()
logger.info(f"Gemini API-Antwort erhalten (Status: {response.status_code}).")
text = res_json['candidates'][0]['content']['parts'][0]['text'] text = res_json['candidates'][0]['content']['parts'][0]['text']
if "```json" in text: text = text.split("```json")[1].split("```")[0].strip() result = _extract_json_from_text(text)
return json.loads(text)
if not result:
raise ValueError("Konnte kein valides JSON extrahieren")
return result
except Exception as e: except Exception as e:
logger.error(f"Competitor identification failed: {e}") logger.error(f"Competitor identification failed: {e}")
return {"error": str(e)} return {"localCompetitors": [], "nationalCompetitors": [], "internationalCompetitors": []}
def analyze_company(company_name, strategy, target_market): def analyze_company(company_name, strategy, target_market):
logger.info(f"--- STARTING DEEP TECH AUDIT FOR: {company_name} ---") logger.info(f"--- STARTING DEEP TECH AUDIT FOR: {company_name} ---")
api_key = load_gemini_api_key() api_key = load_gemini_api_key()
GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1/models/gemini-2.5-pro:generateContent?key={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}"
# 1. Website Finding (SerpAPI fallback to Gemini) # 1. Website Finding (SerpAPI fallback to Gemini)
url = None url = None
@@ -235,11 +322,30 @@ def analyze_company(company_name, strategy, target_market):
if not url: if not url:
# Fallback: Frage Gemini (Low Confidence) # Fallback: Frage Gemini (Low Confidence)
logger.info("Keine URL via SerpAPI, frage Gemini...") logger.info("Keine URL via SerpAPI, frage Gemini...")
prompt_url = f"Find the official website URL for '{company_name}' in '{target_market}'. Output ONLY the URL." prompt_url = f"What is the official homepage URL for the company '{company_name}' in the market '{target_market}'? Respond with ONLY the single, complete URL and nothing else."
payload_url = {"contents": [{"parts": [{"text": prompt_url}]}]}
logger.info("Sende Anfrage an Gemini API (URL Fallback)...")
# logger.debug(f"Rohe Gemini API-Anfrage (JSON): {json.dumps(payload_url, indent=2)}")
try: try:
res = requests.post(GEMINI_API_URL, json={"contents": [{"parts": [{"text": prompt_url}]}]}, headers={'Content-Type': 'application/json'}) res = requests.post(GEMINI_API_URL, json=payload_url, headers={'Content-Type': 'application/json'}, timeout=15)
url = res.json()['candidates'][0]['content']['parts'][0]['text'].strip() res.raise_for_status()
except: pass res_json = res.json()
logger.info(f"Gemini API-Antwort erhalten (Status: {res.status_code}).")
candidate = res_json.get('candidates', [{}])[0]
content = candidate.get('content', {}).get('parts', [{}])[0]
text_response = content.get('text', '').strip()
url_match = re.search(r'(https?://[^\s"]+)', text_response)
if url_match:
url = url_match.group(1)
logger.info(f"Gemini Fallback hat URL gefunden: {url}")
else:
logger.warning(f"Keine gültige URL in Gemini-Antwort gefunden: '{text_response}'")
except Exception as e:
logger.error(f"Gemini URL Fallback failed: {e}")
pass
if not url or not url.startswith("http"): if not url or not url.startswith("http"):
return {"error": f"Could not find website for {company_name}"} return {"error": f"Could not find website for {company_name}"}
@@ -248,25 +354,72 @@ def analyze_company(company_name, strategy, target_market):
homepage_text = get_website_text(url) homepage_text = get_website_text(url)
if not homepage_text: if not homepage_text:
return {"error": f"Could not scrape website {url}"} return {"error": f"Could not scrape website {url}"}
homepage_text = re.sub(r'[^\x20-\x7E\n\r\t]', '', homepage_text)
# 3. Targeted Signal Search (The "Hunter" Phase) # --- ENHANCED: EXTERNAL TECHNOGRAPHIC INTELLIGENCE ---
# Suche aktiv nach Wettbewerbern, nicht nur auf der Firmenwebsite.
tech_evidence = []
# Liste bekannter Wettbewerber / Incumbents
known_incumbents = [
"SAP Ariba", "Jaggaer", "Coupa", "SynerTrade", "Ivalua",
"ServiceNow", "Salesforce", "Oracle SCM", "Zycus", "GEP",
"SupplyOn", "EcoVadis", "IntegrityNext"
]
# Suche 1: Direkte Verbindung zu Software-Anbietern (Case Studies, News, etc.)
# Wir bauen eine Query mit OR, um API-Calls zu sparen.
# Splitte in 2 Gruppen, um Query-Länge im Rahmen zu halten
half = len(known_incumbents) // 2
group1 = " OR ".join([f'"{inc}"' for inc in known_incumbents[:half]])
group2 = " OR ".join([f'"{inc}"' for inc in known_incumbents[half:]])
tech_queries = [
f'"{company_name}" ({group1})',
f'"{company_name}" ({group2})',
f'"{company_name}" "supplier portal" login' # Suche nach dem Portal selbst
]
logger.info(f"Starte erweiterte Tech-Stack-Suche für {company_name}...")
for q in tech_queries:
logger.info(f"Tech Search: {q}")
results = serp_search(q, num_results=4) # Etwas mehr Ergebnisse
if results:
for r in results:
tech_evidence.append(f"- Found: {r['title']}\n Snippet: {r['snippet']}\n Link: {r['link']}")
tech_evidence_text = "\n".join(tech_evidence)
# --- END ENHANCED TECH SEARCH ---
# 3. Targeted Signal Search (The "Hunter" Phase) - Basierend auf Strategy
signal_evidence = [] signal_evidence = []
# Firmographics Search # Firmographics Search
firmographics_results = serp_search(f"{company_name} Umsatz Mitarbeiterzahl 2023") firmographics_results = serp_search(f"{company_name} Umsatz Mitarbeiterzahl 2023")
firmographics_context = "\n".join([f"- {r['snippet']} ({r['link']})" for r in firmographics_results]) firmographics_context = "\n".join([f"- {r['snippet']} ({r['link']})" for r in firmographics_results])
# Signal Searches # Signal Searches (Original Strategy)
signals = strategy.get('signals', []) signals = strategy.get('signals', [])
for signal in signals: for signal in signals:
# Überspringe Signale, die wir schon durch die Tech-Suche massiv abgedeckt haben,
# es sei denn, sie sind sehr spezifisch.
if "incumbent" in signal['id'].lower() or "tech" in signal['id'].lower():
logger.info(f"Skipping generic signal search '{signal['name']}' in favor of Enhanced Tech Search.")
continue
proof_strategy = signal.get('proofStrategy', {}) proof_strategy = signal.get('proofStrategy', {})
query_template = proof_strategy.get('searchQueryTemplate') query_template = proof_strategy.get('searchQueryTemplate')
search_context = "" search_context = ""
if query_template: if query_template:
# Domain aus URL extrahieren für bessere Queries (z.B. site:firma.de) try:
domain = url.split("//")[-1].split("/")[0].replace("www.", "") domain = url.split("//")[-1].split("/")[0].replace("www.", "")
query = query_template.replace("{{COMPANY}}", company_name).replace("{{domain}}", domain) except:
domain = ""
query = query_template.replace("{{COMPANY}}", company_name).replace("{COMPANY}", company_name)
query = query.replace("{{domain}}", domain).replace("{domain}", domain)
logger.info(f"Signal Search '{signal['name']}': {query}") logger.info(f"Signal Search '{signal['name']}': {query}")
results = serp_search(query, num_results=3) results = serp_search(query, num_results=3)
@@ -280,31 +433,39 @@ def analyze_company(company_name, strategy, target_market):
evidence_text = "\n\n".join(signal_evidence) evidence_text = "\n\n".join(signal_evidence)
prompt = f""" prompt = f"""
You are a B2B Market Intelligence Auditor. You are a Strategic B2B Sales Consultant.
Audit the company '{company_name}' ({url}) based on the collected evidence. Analyze the company '{company_name}' ({url}) to create a "best-of-breed" sales pitch strategy.
--- STRATEGY (Signals to find) --- --- STRATEGY (What we are looking for) ---
{json.dumps(signals, indent=2)} {json.dumps(signals, indent=2)}
--- EVIDENCE SOURCE 1: HOMEPAGE CONTENT --- --- EVIDENCE 1: EXTERNAL TECH-STACK INTELLIGENCE (CRITICAL) ---
{homepage_text[:10000]} Look closely here for mentions of competitors like SAP Ariba, Jaggaer, SynerTrade, Coupa, etc.
{tech_evidence_text}
--- EVIDENCE SOURCE 2: FIRMOGRAPHICS SEARCH --- --- EVIDENCE 2: HOMEPAGE CONTENT ---
{homepage_text[:8000]}
--- EVIDENCE 3: FIRMOGRAPHICS SEARCH ---
{firmographics_context} {firmographics_context}
--- EVIDENCE SOURCE 3: TARGETED SIGNAL SEARCH RESULTS --- --- EVIDENCE 4: TARGETED SIGNAL SEARCH RESULTS ---
{evidence_text} {evidence_text}
---------------------------------- ----------------------------------
TASK: TASK:
1. **Firmographics**: Estimate Revenue and Employees based on Source 1 & 2. Be realistic. Use buckets if unsure. 1. **Firmographics**: Estimate Revenue and Employees.
2. **Status**: Determine 'status' (Bestandskunde, Nutzt Wettbewerber, Greenfield, Unklar). 2. **Technographic Audit**: Look for specific competitor software or legacy systems mentioned in EVIDENCE 1 (e.g., "Partner of SynerTrade", "Login to Jaggaer Portal").
3. **Evaluate Signals**: For each signal, decide 'value' (Yes/No/Partial). 3. **Status**:
- **CRITICAL**: You MUST cite your source for the 'proof'. - Set to "Nutzt Wettbewerber" if ANY competitor technology is found (Ariba, Jaggaer, SynerTrade, Coupa, etc.).
- If found in Source 3 (Search), write: "Found in job posting/doc: [Snippet]" and include the URL. - Set to "Greenfield" ONLY if absolutely no competitor tech is found.
- If found in Source 1 (Homepage), write: "On homepage: [Quote]". - Set to "Bestandskunde" if they already use our solution.
- If not found, write: "Not found". 4. **Evaluate Signals**: For each signal, provide a "value" (Yes/No/Partial) and "proof".
4. **Recommendation**: 1-sentence verdict. 5. **Recommendation (Pitch Strategy)**:
- DO NOT write a generic verdict.
- If they use a competitor (e.g., Ariba), explain how to position against it (e.g., "Pitch as a specialized add-on for logistics, filling Ariba's gaps").
- If Greenfield, explain the entry point.
- **Tone**: Strategic, insider-knowledge, specific.
STRICTLY output only JSON: STRICTLY output only JSON:
{{ {{
@@ -326,21 +487,134 @@ def analyze_company(company_name, strategy, target_market):
} }
try: try:
logger.info("Sende Audit-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()
response_text = response_data['candidates'][0]['content']['parts'][0]['text'] logger.info(f"Gemini API-Antwort erhalten (Status: {response.status_code}).")
if response_text.startswith('```json'): text = response_data['candidates'][0]['content']['parts'][0]['text']
response_text = response_text.split('```json')[1].split('```')[0].strip() result = _extract_json_from_text(text)
if not result:
raise ValueError("Konnte kein valides JSON extrahieren")
result = json.loads(response_text) result['dataSource'] = "Digital Trace Audit (Deep Dive)"
result['dataSource'] = "Digital Trace Audit (Deep Dive)" # Mark as verified
logger.info(f"Audit für {company_name} erfolgreich abgeschlossen.") logger.info(f"Audit für {company_name} erfolgreich abgeschlossen.")
return result return result
except Exception as e: except Exception as e:
logger.error(f"Audit failed for {company_name}: {e}") logger.error(f"Audit failed for {company_name}: {e}")
return {"error": str(e)} return {
"companyName": company_name,
"status": "Unklar / Manuelle Prüfung",
"revenue": "Error",
"employees": "Error",
"tier": "Tier 3",
"dynamicAnalysis": {},
"recommendation": f"Audit failed due to API Error: {str(e)}",
"dataSource": "Error"
}
def generate_outreach_campaign(company_data_json, knowledge_base_content, reference_url):
"""
Erstellt personalisierte E-Mail-Kampagnen basierend auf Audit-Daten und einer strukturierten Wissensdatenbank.
Generiert spezifische Ansprachen für verschiedene Rollen (Personas).
"""
company_name = company_data_json.get('companyName', 'Unknown')
logger.info(f"--- STARTING ROLE-BASED OUTREACH GENERATION FOR: {company_name} ---")
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}"
prompt = f"""
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}'.
--- 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}
--- INPUT 2: THE TARGET COMPANY (Audit Facts) ---
{json.dumps(company_data_json, indent=2)}
--- INPUT 3: THE REFERENCE CLIENT (Social Proof) ---
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).
You MUST mention this Reference Client by name (derive it from the URL, e.g., 'schindler.com' -> 'Schindler') to establish trust.
--- TASK ---
1. **Analyze**: Match the Target Company (Input 2) to the most relevant 'Zielbranche/Segment' from the Knowledge Base (Input 1).
2. **Select Roles**: Identify the top 2 most distinct and relevant 'Rollen' (Personas) from the Knowledge Base for this specific company situation.
- *Example:* If the audit says they use a competitor (risk of lock-in), select a role like "Strategic Purchaser" or "Head of R&D" who cares about "Second Source".
- *Example:* If they have quality issues or complex logistics, pick "Quality Manager" or "Logistics Head".
3. **Draft Campaigns**: For EACH of the 2 selected roles, write a 3-step email sequence.
--- TONE & STYLE GUIDELINES (CRITICAL) ---
- **Perspective:** Operational Expert & Insider. NOT generic marketing.
- **Be Gritty & Specific:** Do NOT use fluff like "optimize efficiency" or "streamline processes" without context.
- 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:**
1. "I noticed [Fact from Audit/Tech Stack]..." (e.g., "You rely on PDF orders via Jaggaer...")
2. "In [Industry], this often leads to [Operational Pain]..." (e.g., "missing ASNs causing delays at the hub.")
3. "We helped [Reference Client Name] solve exactly this by [Specific Solution]..."
4. "Let's discuss how to get [Operational Gain] without replacing your ERP."
- **Mandatory Social Proof:** You MUST mention the Reference Client Name (from Input 3) in the email body or footer.
- **Language:** German (as the inputs are German).
--- OUTPUT FORMAT (Strictly JSON) ---
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 = {
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {"response_mime_type": "application/json"}
}
try:
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.raise_for_status()
response_data = response.json()
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']
result = _extract_json_from_text(text)
if not result:
raise ValueError("Konnte kein valides JSON extrahieren")
return result
except Exception as e:
logger.error(f"Campaign generation failed for {company_name}: {e}")
return [{"error": str(e)}]
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@@ -351,6 +625,7 @@ 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
args = parser.parse_args() args = parser.parse_args()
if args.mode == "generate_strategy": if args.mode == "generate_strategy":
@@ -365,6 +640,11 @@ def main():
elif args.mode == "analyze_company": elif args.mode == "analyze_company":
strategy = json.loads(args.strategy_json) strategy = json.loads(args.strategy_json)
print(json.dumps(analyze_company(args.company_name, strategy, args.target_market))) print(json.dumps(analyze_company(args.company_name, strategy, args.target_market)))
elif args.mode == "generate_outreach":
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()
print(json.dumps(generate_outreach_campaign(company_data, knowledge_base, args.reference_url)))
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -706,4 +706,4 @@ Der Prozess für den Benutzer bleibt weitgehend gleich, ist aber technisch solid
**Schritt 4 & 5: Reporting & Personalisierte Ansprache** **Schritt 4 & 5: Reporting & Personalisierte Ansprache**
- **Ergebnis-Darstellung:** Die faktenbasierten Analyseergebnisse werden im Frontend angezeigt. - **Ergebnis-Darstellung:** Die faktenbasierten Analyseergebnisse werden im Frontend angezeigt.
- **Kampagnen-Generierung:** Die KI nutzt die validierten "Digitalen Signale" als Aufhänger, um hyper-personalisierte und extrem treffsichere E-Mail-Entwürfe zu erstellen. - **Kampagnen-Generierung:** Die KI nutzt die validierten "Digitalen Signale" als Aufhänger, um hyper-personalisierte und extrem treffsichere E-Mail-Entwürfe zu erstellen. Dabei werden **operative Schmerzpunkte ("Grit")** und **Social Proof** (Referenzkunden) aggressiv genutzt, um Insider-Status zu demonstrieren.