general-market-intelligence/services/geminiService.ts hinzugefügt
This commit is contained in:
339
general-market-intelligence/services/geminiService.ts
Normal file
339
general-market-intelligence/services/geminiService.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import { LeadStatus, AnalysisResult, Competitor, Language, Tier, EmailDraft, SearchStrategy, SearchSignal } from "../types";
|
||||
|
||||
const apiKey = process.env.API_KEY;
|
||||
const ai = new GoogleGenAI({ apiKey: apiKey || '' });
|
||||
|
||||
// Helper to extract JSON
|
||||
const extractJson = (text: string): any => {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/);
|
||||
if (jsonMatch && jsonMatch[1]) {
|
||||
try { return JSON.parse(jsonMatch[1]); } catch (e2) {}
|
||||
}
|
||||
const arrayMatch = text.match(/\[\s*[\s\S]*\s*\]/);
|
||||
if (arrayMatch) {
|
||||
try { return JSON.parse(arrayMatch[0]); } catch (e3) {}
|
||||
}
|
||||
const objectMatch = text.match(/\{\s*[\s\S]*\s*\}/);
|
||||
if (objectMatch) {
|
||||
try { return JSON.parse(objectMatch[0]); } catch (e4) {}
|
||||
}
|
||||
throw new Error("Could not parse JSON response");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* NEW: Generates a search strategy based on the uploaded strategy file and reference URL.
|
||||
*/
|
||||
export const generateSearchStrategy = async (
|
||||
referenceUrl: string,
|
||||
contextContent: string,
|
||||
language: Language
|
||||
): Promise<SearchStrategy> => {
|
||||
if (!apiKey) throw new Error("API Key missing");
|
||||
|
||||
const langInstruction = language === 'de' ? "OUTPUT LANGUAGE: German (Deutsch) for all text fields." : "OUTPUT LANGUAGE: English.";
|
||||
|
||||
const prompt = `
|
||||
I am a B2B Market Intelligence Architect.
|
||||
|
||||
--- STRATEGIC CONTEXT (Uploaded Document) ---
|
||||
${contextContent}
|
||||
---------------------------------------------
|
||||
|
||||
Reference Client URL: "${referenceUrl}"
|
||||
|
||||
Task: Create a "Digital Trace Strategy" to identify high-potential leads based on the Strategic Context and the Reference Client.
|
||||
|
||||
1. ANALYZE the uploaded context (Offer, Personas, Pain Points).
|
||||
2. EXTRACT a 1-sentence summary of what is being sold ("summaryOfOffer").
|
||||
3. DEFINE an Ideal Customer Profile (ICP) derived from the "Target Groups" in the context and the Reference Client.
|
||||
4. **CRITICAL**: Identify 3-5 specific "Digital Signals" (Traces) visible on a company's website that indicate a match for the Pain Points/Needs defined in the context.
|
||||
- Use the "Pain Points" and "Offer" from the context to derive these signals.
|
||||
- Signals must be checkable via public web data.
|
||||
- Example: If the context mentions "Pain: High return rates", Signal could be "Complex Return Policy in Footer" or "No automated return portal".
|
||||
|
||||
${langInstruction}
|
||||
|
||||
Output JSON format:
|
||||
{
|
||||
"summaryOfOffer": "Short 1-sentence summary of the product/service",
|
||||
"idealCustomerProfile": "Detailed ICP based on context",
|
||||
"signals": [
|
||||
{
|
||||
"id": "sig_1",
|
||||
"name": "Short Name (e.g. 'Tech Stack')",
|
||||
"description": "What specifically to look for? (e.g. 'Look for Shopify in source code')",
|
||||
"targetPageKeywords": ["tech", "career", "about", "legal"]
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash',
|
||||
contents: prompt,
|
||||
config: { temperature: 0.4, responseMimeType: "application/json" }
|
||||
});
|
||||
|
||||
const data = extractJson(response.text || "{}");
|
||||
return {
|
||||
productContext: data.summaryOfOffer || "Market Analysis", // Use the AI-generated summary
|
||||
idealCustomerProfile: data.idealCustomerProfile || "Companies similar to reference",
|
||||
signals: data.signals || []
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Strategy generation failed", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const identifyCompetitors = async (referenceUrl: string, targetMarket: string, language: Language): Promise<Partial<Competitor>[]> => {
|
||||
if (!apiKey) throw new Error("API Key missing");
|
||||
|
||||
const langInstruction = language === 'de' ? "OUTPUT LANGUAGE: German." : "OUTPUT LANGUAGE: English.";
|
||||
|
||||
// UPDATED: Reduced count to 10 for speed
|
||||
const prompt = `
|
||||
Goal: Identify 10 DIRECT COMPETITORS or LOOKALIKES for the company found at URL: "${referenceUrl}" in "${targetMarket}".
|
||||
|
||||
${langInstruction}
|
||||
|
||||
Rules:
|
||||
1. Focus on the same business model (e.g. Retailer vs Retailer, Brand vs Brand).
|
||||
2. Exclude the reference company itself.
|
||||
|
||||
Return JSON array: [{ "name": "...", "url": "...", "description": "..." }]
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash',
|
||||
contents: prompt,
|
||||
config: { tools: [{ googleSearch: {} }], temperature: 0.4 }
|
||||
});
|
||||
|
||||
const companies = extractJson(response.text || "[]");
|
||||
return Array.isArray(companies) ? companies : [];
|
||||
} catch (error) {
|
||||
console.error("Competitor search failed", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* UPDATED: Dynamic Analysis based on Strategy
|
||||
*/
|
||||
export const analyzeCompanyWithStrategy = async (
|
||||
companyName: string,
|
||||
strategy: SearchStrategy,
|
||||
language: Language
|
||||
): Promise<AnalysisResult> => {
|
||||
if (!apiKey) throw new Error("API Key missing");
|
||||
|
||||
const langInstruction = language === 'de' ? "OUTPUT LANGUAGE: German." : "OUTPUT LANGUAGE: English.";
|
||||
|
||||
// Construct the signals prompt part
|
||||
const signalsPrompt = strategy.signals.map(s =>
|
||||
`- Signal "${s.name}" (${s.id}): Look for ${s.description}. search queries like "${companyName} ${s.targetPageKeywords.join(' ')}"`
|
||||
).join('\n');
|
||||
|
||||
const prompt = `
|
||||
Perform a Deep Dive Analysis on "${companyName}".
|
||||
|
||||
Context: We are selling "${strategy.productContext}".
|
||||
|
||||
${langInstruction}
|
||||
|
||||
--- STEP 1: FIRMOGRAPHICS (Waterfall) ---
|
||||
Find Revenue and Employee count using Wikipedia -> Corp Site -> Web Search.
|
||||
Classify Tier: Tier 1 (>100M), Tier 2 (>10M), Tier 3 (<10M).
|
||||
|
||||
--- STEP 2: DIGITAL TRACES (CUSTOM SIGNALS) ---
|
||||
Investigate the following specific signals:
|
||||
${signalsPrompt}
|
||||
|
||||
--- STEP 3: STATUS & RECOMMENDATION ---
|
||||
Based on findings, determine Lead Status (Customer, Competitor, Potential).
|
||||
Write a 1-sentence sales recommendation.
|
||||
|
||||
Output JSON:
|
||||
{
|
||||
"companyName": "${companyName}",
|
||||
"revenue": "...",
|
||||
"employees": "...",
|
||||
"dataSource": "...",
|
||||
"tier": "Tier 1|Tier 2|Tier 3",
|
||||
"status": "Bestandskunde|Nutzt Wettbewerber|Greenfield / Potenzial|Unklar",
|
||||
"recommendation": "...",
|
||||
"dynamicAnalysis": {
|
||||
"${strategy.signals[0]?.id || 'sig_1'}": { "value": "Short finding", "proof": "Evidence found", "sentiment": "Positive|Neutral|Negative" },
|
||||
... (for all signals)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash',
|
||||
contents: prompt,
|
||||
config: {
|
||||
tools: [{ googleSearch: {} }],
|
||||
temperature: 0.1,
|
||||
}
|
||||
});
|
||||
|
||||
const text = response.text || "{}";
|
||||
const data = extractJson(text);
|
||||
|
||||
// Get Sources
|
||||
const sources: string[] = [];
|
||||
response.candidates?.[0]?.groundingMetadata?.groundingChunks?.forEach((chunk: any) => {
|
||||
if (chunk.web?.uri) sources.push(chunk.web.uri);
|
||||
});
|
||||
|
||||
// Tier mapping logic
|
||||
let mappedTier = Tier.TIER_3;
|
||||
if (data.tier?.includes("Tier 1")) mappedTier = Tier.TIER_1;
|
||||
else if (data.tier?.includes("Tier 2")) mappedTier = Tier.TIER_2;
|
||||
|
||||
// Status mapping logic
|
||||
let mappedStatus = LeadStatus.UNKNOWN;
|
||||
const statusStr = (data.status || "").toLowerCase();
|
||||
if (statusStr.includes("bestand") || statusStr.includes("customer")) mappedStatus = LeadStatus.CUSTOMER;
|
||||
else if (statusStr.includes("wettbewerb") || statusStr.includes("competitor")) mappedStatus = LeadStatus.COMPETITOR;
|
||||
else if (statusStr.includes("greenfield") || statusStr.includes("poten")) mappedStatus = LeadStatus.POTENTIAL;
|
||||
|
||||
return {
|
||||
companyName: data.companyName || companyName,
|
||||
status: mappedStatus,
|
||||
revenue: data.revenue || "?",
|
||||
employees: data.employees || "?",
|
||||
tier: mappedTier,
|
||||
dataSource: data.dataSource || "Web",
|
||||
dynamicAnalysis: data.dynamicAnalysis || {},
|
||||
recommendation: data.recommendation || "Check manually",
|
||||
sources: sources.slice(0, 3),
|
||||
processingChecks: {
|
||||
wiki: (data.dataSource || "").toLowerCase().includes("wiki"),
|
||||
revenue: !!data.revenue,
|
||||
signalsChecked: true
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Analysis failed for ${companyName}`, error);
|
||||
return {
|
||||
companyName,
|
||||
status: LeadStatus.UNKNOWN,
|
||||
revenue: "?",
|
||||
employees: "?",
|
||||
tier: Tier.TIER_3,
|
||||
dataSource: "Error",
|
||||
dynamicAnalysis: {},
|
||||
recommendation: "Analysis Error",
|
||||
processingChecks: { wiki: false, revenue: false, signalsChecked: false }
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const generateOutreachCampaign = async (
|
||||
companyData: AnalysisResult,
|
||||
knowledgeBase: string,
|
||||
language: Language,
|
||||
referenceUrl: string
|
||||
): Promise<EmailDraft[]> => {
|
||||
if (!apiKey) throw new Error("API Key missing");
|
||||
|
||||
// Format dynamic data for the prompt
|
||||
let insights = "";
|
||||
if (companyData.dynamicAnalysis) {
|
||||
Object.entries(companyData.dynamicAnalysis).forEach(([key, val]) => {
|
||||
// CLEAN INPUT: Do not pass internal signal IDs like sig_1 to the LLM's context if possible, or instruct it to ignore them.
|
||||
// Here we just pass the observation and value.
|
||||
insights += `- Observation: ${val.value} (Proof: ${val.proof})\n`;
|
||||
});
|
||||
}
|
||||
|
||||
const combined = `
|
||||
TARGET COMPANY: ${companyData.companyName}
|
||||
SIZE: ${companyData.revenue}, ${companyData.employees}
|
||||
KEY OBSERVATIONS (Web Signals):
|
||||
${insights}
|
||||
`;
|
||||
|
||||
const langInstruction = language === 'de'
|
||||
? "OUTPUT LANGUAGE: German (Deutsch). Tone: Professional, polite (Sie-form), persuasive."
|
||||
: "OUTPUT LANGUAGE: English. Tone: Professional, persuasive.";
|
||||
|
||||
const prompt = `
|
||||
You are a top-tier B2B Copywriter.
|
||||
|
||||
OBJECTIVE: Write 3 distinct cold email drafts to contact "${companyData.companyName}".
|
||||
|
||||
INPUTS:
|
||||
1. STRATEGIC CONTEXT (Offer, Value Prop, Case Studies):
|
||||
${knowledgeBase}
|
||||
|
||||
2. TARGET DATA (The recipient):
|
||||
${combined}
|
||||
|
||||
3. REFERENCE CLIENT (Social Proof):
|
||||
URL: "${referenceUrl}"
|
||||
(Extract the company name from this URL to use as the Success Story / Reference)
|
||||
|
||||
${langInstruction}
|
||||
|
||||
MANDATORY RULES:
|
||||
1. **NO TECHNICAL PLACEHOLDERS**: STRICTLY FORBIDDEN to use "(sig_1)", "(sig_x)", "[Name]" or similar. The text must be 100% ready to send.
|
||||
|
||||
2. **MANDATORY DYNAMIC SOCIAL PROOF**: You MUST include a specific paragraph referencing the "Reference Client" provided above, but **ADAPTED TO THE PERSONA** of the email.
|
||||
- **CRITICAL**: Do NOT use the same generic KPI for all emails.
|
||||
- **LOGIC**: Look at the "Strategic Context" for KPIs.
|
||||
- If the persona is **Operational** (COO, Ops Manager): Focus on **Efficiency, Speed, Automation** (e.g. "Did you know [Reference] saved 20% time...?").
|
||||
- If the persona is **HR / People**: Focus on **Staff Retention, Workload Relief** (e.g. "Did you know [Reference] relieved their staff by X hours...?").
|
||||
- If the persona is **Strategic / General**: Focus on **Cost, Revenue, Innovation** (e.g. "Did you know [Reference] increased ROI by...?").
|
||||
|
||||
- Structure (German): "Übrigens: Wussten Sie, dass [Reference Client Name] [Persona-relevant KPI]...?"
|
||||
- Structure (English): "By the way: Did you know that [Reference Client Name] [Persona-relevant KPI]...?"
|
||||
|
||||
3. **HYPER-PERSONALIZATION**: Use the "Key Observations" (Web Signals) to connect the problem to the solution. Don't just list them.
|
||||
|
||||
Output JSON format:
|
||||
[{ "persona": "Target Role (e.g. COO)", "subject": "...", "body": "...", "keyPoints": ["Used observation X", "Mentioned Reference Client (Persona adjusted)"] }]
|
||||
`;
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash',
|
||||
contents: prompt,
|
||||
config: { temperature: 0.7, responseMimeType: "application/json" }
|
||||
});
|
||||
|
||||
const drafts = extractJson(response.text || "[]");
|
||||
return Array.isArray(drafts) ? drafts : [];
|
||||
} catch (error) {
|
||||
console.error("Outreach generation failed", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const translateEmailDrafts = async (drafts: EmailDraft[], targetLanguage: Language): Promise<EmailDraft[]> => {
|
||||
if (!apiKey) throw new Error("API Key missing");
|
||||
const langName = targetLanguage === 'de' ? 'German' : targetLanguage === 'fr' ? 'French' : 'English';
|
||||
const prompt = `Translate this JSON array to ${langName}. Keep JSON structure. Input: ${JSON.stringify(drafts)}`;
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash',
|
||||
contents: prompt,
|
||||
config: { responseMimeType: "application/json" }
|
||||
});
|
||||
return extractJson(response.text || "[]");
|
||||
} catch (e) { return drafts; }
|
||||
}
|
||||
Reference in New Issue
Block a user