feat: integrate Gemini Pro 1.5 with KAM persona for data analysis

This commit is contained in:
Christian Vidal Wolf
2026-02-10 15:15:44 +01:00
parent 96e9583fdb
commit 3f6fdc8d72
2 changed files with 69 additions and 61 deletions
+37 -29
View File
@@ -6,17 +6,25 @@ import { AggregatedData } from "./types";
declare const process: any;
const SYSTEM_INSTRUCTION = `
You are a senior data analyst assistant for a retail dashboard called "Craze Analytix".
You have access to a detailed report of the currently filtered sales data.
The data includes Sell Out (Revenue in €), Units Sold, Product Lines, Customers/Markets, and Seasonality trends.
You are an expert Key Account Manager (KAM) and Senior Sales Strategist for "Craze Analytix".
Your specialty is analyzing retail data, advertising performance, and market trends to provide high-level strategic recommendations.
Your goal is to answer user questions specific to the provided data.
You have access to a detailed report of the currently filtered sales and advertising data.
The data includes Sell Out (€), Units Sold, Product Lines, Customer Markets, Seasonality (Monthly Trends), and Growth/Decline metrics.
Your objective:
1. **Analyze**: Deeply study the provided data according to the user's specific query.
2. **Opinion**: Provide expert opinions on the health of the business, product performance, or market position.
3. **Recommendations**: Give actionable, professional advice to improve sales, ROAS, stock health, or market share.
Guidelines:
- Maintain a professional, consultative, and business-driven tone (KAM style).
- Always justify your recommendations with specific numbers from the data.
- If asked about "Trends" or "Seasonality", look at the Monthly Seasonality section.
- If asked about "Growth" or "Decline", look at the Top/Bottom Movers sections.
- If asked about specific Product Lines, look at the Product Line Breakdown.
- Always format numbers clearly (e.g., "€1.2M", "€5,200", "15k units").
- When comparing years, calculate the percentage difference if not explicitly provided.
- Keep answers professional, concise, and business-focused.
- When comparing metrics, calculate variances or ratios if relevant (e.g. Sales vs Ads growth).
- Format your response with clear headers and bullet points for readability.
- Respond in the same language as the user's question (e.g., if they ask in Spanish, answer in Spanish).
`;
// Helper to get API key safely
@@ -28,15 +36,15 @@ const getApiKey = (): string | undefined => {
}
};
const formatCurrency = (val: number) => `${val.toLocaleString('de-DE', {minimumFractionDigits: 0, maximumFractionDigits: 0})}`;
const formatCurrency = (val: number) => `${val.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
const formatUnits = (val: number) => `${val.toLocaleString('de-DE')} units`;
export const queryGemini = async (
question: string,
context: AggregatedData,
question: string,
context: AggregatedData,
filteredRecordCount: number
): Promise<string> => {
const apiKey = getApiKey();
if (!apiKey) {
@@ -48,39 +56,39 @@ export const queryGemini = async (
// --- CONTEXT GENERATION ---
// We construct a structured report mirroring the dashboard charts
// 1. Totals by Year (KPI Cards)
const yearlySummary = Object.entries(context.totalsByYear)
.sort((a, b) => parseInt(b[0]) - parseInt(a[0])) // Descending years
.map(([year, data]) => ` - ${year}: ${formatCurrency(data.sellOut)} | ${formatUnits(data.units)}`)
.join('\n');
.sort((a, b) => parseInt(b[0]) - parseInt(a[0])) // Descending years
.map(([year, data]) => ` - ${year}: ${formatCurrency(data.sellOut)} | ${formatUnits(data.units)}`)
.join('\n');
// 2. Seasonality (Line Chart Data)
// We simplify this to a CSV-like list for the AI to parse trends
const seasonalitySummary = context.seasonality.map(p => {
// Extract values for each year in the point
const yearValues = context.availableYears.map(y => `${y}: ${formatCurrency(p[y] as number || 0)}`).join(', ');
return ` - ${p.name}: [${yearValues}]`;
// Extract values for each year in the point
const yearValues = context.availableYears.map(y => `${y}: ${formatCurrency(p[y] as number || 0)}`).join(', ');
return ` - ${p.name}: [${yearValues}]`;
}).join('\n');
// 3. Top Movers (Growth Table) - Limit to Top 10
const growthSummary = context.topMovers.slice(0, 10).map(m =>
` - ${m.line}: +€${m.sellOutGrowthValue.toLocaleString('de-DE')} (${m.sellOutGrowthPercentage.toFixed(1)}%)`
const growthSummary = context.topMovers.slice(0, 10).map(m =>
` - ${m.line}: +€${m.sellOutGrowthValue.toLocaleString('de-DE')} (${m.sellOutGrowthPercentage.toFixed(1)}%)`
).join('\n');
// 4. Declining Movers (Decline Table) - Limit to Top 10
const declineSummary = context.bottomMovers.slice(0, 10).map(m =>
` - ${m.line}: -€${Math.abs(m.sellOutGrowthValue).toLocaleString('de-DE')} (${m.sellOutGrowthPercentage.toFixed(1)}%)`
const declineSummary = context.bottomMovers.slice(0, 10).map(m =>
` - ${m.line}: -€${Math.abs(m.sellOutGrowthValue).toLocaleString('de-DE')} (${m.sellOutGrowthPercentage.toFixed(1)}%)`
).join('\n');
// 5. Product Lines Overview (Bar Charts) - Limit to Top 50 to save tokens but give depth
const topLinesSummary = context.byLine.slice(0, 50).map((l, i) =>
` ${i+1}. ${l.name}: ${formatCurrency(l.value)} | ${formatUnits(l.units)}`
const topLinesSummary = context.byLine.slice(0, 50).map((l, i) =>
` ${i + 1}. ${l.name}: ${formatCurrency(l.value)} | ${formatUnits(l.units)}`
).join('\n');
// 6. Customer Distribution (Customer Chart)
const customerSummary = context.byCustomer.map(c =>
` - ${c.name}: ${formatCurrency(c.value)}`
const customerSummary = context.byCustomer.map(c =>
` - ${c.name}: ${formatCurrency(c.value)}`
).join('\n');
const fullReport = `
@@ -122,9 +130,9 @@ ${customerSummary}
return response.text || "I couldn't generate a response based on the data provided.";
} catch (error: any) {
console.error("Gemini API Error:", error);
if (error.message && error.message.includes("Not implemented on this platform")) {
return "System Error: The AI SDK detected a platform mismatch.";
return "System Error: The AI SDK detected a platform mismatch.";
}
return `Error: ${error.message || "An unexpected error occurred while analyzing the data."}`;