diff --git a/general-market-intelligence/services/geminiService.ts b/general-market-intelligence/services/geminiService.ts new file mode 100644 index 00000000..bd04c175 --- /dev/null +++ b/general-market-intelligence/services/geminiService.ts @@ -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 => { + 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[]> => { + 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 => { + 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 => { + 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 => { + 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; } +}