Spaces:
Sleeping
Sleeping
| """ | |
| DataMind AI β Groq API Interaction Module | |
| Handles all AI-powered analysis, captions, chat, and insight generation. | |
| """ | |
| import os | |
| import json | |
| from typing import List, Dict, Any, Optional | |
| try: | |
| from openai import OpenAI | |
| except ImportError: | |
| OpenAI = None | |
| try: | |
| from groq import Groq | |
| except ImportError: | |
| Groq = None | |
| def _get_client_and_model() -> tuple: | |
| """Get the appropriate AI client and model name based on available API keys. | |
| Prioritizes Groq (for free usage), falls back to OpenAI if Groq key is missing.""" | |
| # Try Groq first | |
| groq_key = os.environ.get("GROQ_API_KEY") | |
| if groq_key and Groq is not None: | |
| try: | |
| return Groq(api_key=groq_key), "llama-3.3-70b-versatile" | |
| except Exception: | |
| pass | |
| # Fallback to OpenAI | |
| openai_key = os.environ.get("OPENAI_API_KEY") | |
| if openai_key and OpenAI is not None: | |
| try: | |
| return OpenAI(api_key=openai_key), "gpt-4o-mini" | |
| except Exception: | |
| pass | |
| return None, None | |
| def _call_openai(messages: List[Dict], max_tokens: int = 1024, temperature: float = 0.7) -> str: | |
| """Make a call to the AI API (works dynamically for both Groq and OpenAI).""" | |
| client, model_name = _get_client_and_model() | |
| if client is None: | |
| return "" | |
| try: | |
| response = client.chat.completions.create( | |
| model=model_name, | |
| messages=messages, | |
| max_tokens=max_tokens, | |
| temperature=temperature, | |
| ) | |
| return response.choices[0].message.content.strip() | |
| except Exception as e: | |
| print(f"[AI API Error]: {e}") | |
| return "" | |
| def get_chart_caption(chart_title: str, chart_description: str, data_summary: str) -> str: | |
| """Generate a 1-2 line AI insight caption for a chart.""" | |
| messages = [ | |
| { | |
| "role": "system", | |
| "content": ( | |
| "You are a senior data analyst. Generate a concise 1-2 line insight caption for a chart. " | |
| "Be specific with numbers and actionable. Do NOT use markdown formatting. " | |
| "Do NOT start with 'This chart shows'. Instead, state the key finding directly." | |
| ) | |
| }, | |
| { | |
| "role": "user", | |
| "content": ( | |
| f"Chart Title: {chart_title}\n" | |
| f"Chart Description: {chart_description}\n" | |
| f"Dataset Context: {data_summary}\n\n" | |
| "Write a concise 1-2 line analytical insight." | |
| ) | |
| } | |
| ] | |
| result = _call_openai(messages, max_tokens=150, temperature=0.6) | |
| if not result: | |
| return f"Key patterns visible in {chart_title.lower()} β explore the data for deeper trends." | |
| return result | |
| def chat_with_analyst( | |
| question: str, | |
| dataset_summary: str, | |
| chat_history: List[Dict[str, str]], | |
| sample_data: str = "" | |
| ) -> Dict[str, Any]: | |
| """ | |
| AI chat analyst β answers questions about the dataset. | |
| Returns dict with 'answer' and optionally 'chart_type' if a chart is implied. | |
| """ | |
| system_prompt = ( | |
| "You are DataMind AI, a senior data analyst assistant. You analyze datasets and provide " | |
| "clear, actionable insights. You have access to the following dataset information:\n\n" | |
| f"{dataset_summary}\n\n" | |
| ) | |
| if sample_data: | |
| system_prompt += f"Sample rows:\n{sample_data}\n\n" | |
| system_prompt += ( | |
| "Rules:\n" | |
| "1. Always be specific β cite numbers, percentages, column names\n" | |
| "2. If the question implies a chart, include the line: [CHART_SUGGESTED: chart_type] at the end " | |
| "(e.g., [CHART_SUGGESTED: bar_chart], [CHART_SUGGESTED: line_chart], [CHART_SUGGESTED: pie_chart])\n" | |
| "3. Provide actionable recommendations when relevant\n" | |
| "4. If you cannot answer with the available data, explain what additional data would be needed\n" | |
| "5. Do NOT use markdown headers (##). Use plain text with bullet points if needed.\n" | |
| ) | |
| messages = [{"role": "system", "content": system_prompt}] | |
| # Add chat history (last 10 messages for context window management) | |
| for msg in chat_history[-10:]: | |
| messages.append({ | |
| "role": msg.get("role", "user"), | |
| "content": msg.get("content", "") | |
| }) | |
| messages.append({"role": "user", "content": question}) | |
| result = _call_openai(messages, max_tokens=1500, temperature=0.7) | |
| if not result: | |
| # Generate a smart offline response from the dataset summary | |
| result = _generate_offline_response(question, dataset_summary) | |
| # Parse chart suggestion | |
| chart_type = None | |
| if "[CHART_SUGGESTED:" in result: | |
| try: | |
| chart_marker = result.split("[CHART_SUGGESTED:")[1].split("]")[0].strip() | |
| chart_type = chart_marker | |
| result = result.split("[CHART_SUGGESTED:")[0].strip() | |
| except (IndexError, ValueError): | |
| pass | |
| return { | |
| "answer": result, | |
| "chart_type": chart_type | |
| } | |
| def _generate_offline_response(question: str, dataset_summary: str) -> str: | |
| """Generate a useful offline response when the AI API is unavailable.""" | |
| q = question.lower() | |
| # Extract info from the summary | |
| lines = dataset_summary.split("\n") | |
| stats_section = [] | |
| cat_section = [] | |
| date_section = [] | |
| for line in lines: | |
| if "mean=" in line or "median=" in line: | |
| stats_section.append(line.strip()) | |
| elif "unique values" in line: | |
| cat_section.append(line.strip()) | |
| elif "days" in line.lower() and "to" in line: | |
| date_section.append(line.strip()) | |
| # Try to answer based on question keywords | |
| if any(w in q for w in ['trend', 'time', 'monthly', 'seasonal', 'forecast']): | |
| info = date_section[0] if date_section else "date information available in the dataset" | |
| return ( | |
| f"Based on the dataset, here's what I can share about time trends:\n\n" | |
| f"- {info}\n" | |
| f"- Check the Monthly Trend line chart and Seasonal Heatmap for visual patterns.\n" | |
| f"- The Forecast panel below the charts shows projected values for the next 3 months.\n\n" | |
| f"Note: The AI service is temporarily at capacity. Please try again in a few minutes for deeper analysis." | |
| ) | |
| if any(w in q for w in ['best', 'top', 'highest', 'category', 'compare', 'performance']): | |
| info = "\n".join([f"- {c}" for c in cat_section[:3]]) if cat_section else "- Category data is available in the charts" | |
| return ( | |
| f"Here's what I can share about category performance:\n\n{info}\n\n" | |
| f"Check the Bar Chart and Pareto Analysis for rankings, and the Radar Chart for multi-metric comparison.\n\n" | |
| f"Note: The AI service is temporarily at capacity. Please try again shortly for detailed breakdowns." | |
| ) | |
| if any(w in q for w in ['insight', 'finding', 'summary', 'overview', 'heatmap', 'correlation']): | |
| info = "\n".join([f"- {s}" for s in stats_section[:4]]) if stats_section else "- Statistical data visible in the sidebar" | |
| return ( | |
| f"Key statistics from the dataset:\n\n{info}\n\n" | |
| f"The Correlation Heatmap shows relationships between numeric variables. " | |
| f"Look for values close to 1 (strong positive) or -1 (strong negative).\n\n" | |
| f"Note: The AI service is temporarily at capacity. Try again soon for AI-powered interpretations." | |
| ) | |
| if any(w in q for w in ['anomal', 'outlier', 'unusual', 'strange']): | |
| return ( | |
| f"Outlier information is available in the sidebar under 'EDA Report'. " | |
| f"The IQR method was used to flag outliers across all numeric columns.\n\n" | |
| f"Check the Box Plot chart - values shown as circles outside the whiskers are potential outliers.\n\n" | |
| f"Note: The AI service is temporarily at capacity. Please try again soon for deeper anomaly analysis." | |
| ) | |
| # Generic fallback with useful info | |
| summary_preview = "\n".join(lines[:5]) if lines else "Dataset loaded successfully" | |
| return ( | |
| f"Here's a quick overview of your data:\n\n{summary_preview}\n\n" | |
| f"Browse the charts below for visual insights. The sidebar shows detailed EDA results " | |
| f"including missing values, outliers, and key statistics.\n\n" | |
| f"Note: The AI service is temporarily at capacity. Your question has been received - " | |
| f"please try again in a few minutes for a full AI-powered analysis." | |
| ) | |
| def generate_recommendations(dataset_summary: str, eda_results: dict) -> List[Dict[str, str]]: | |
| """Generate AI business recommendations with severity levels.""" | |
| # Build context from EDA | |
| context_parts = [dataset_summary] | |
| if eda_results.get("missing_values"): | |
| context_parts.append(f"Missing values before cleaning: {eda_results['missing_values'].get('total_before', 0)}") | |
| if eda_results.get("outliers"): | |
| outlier_count = sum(o.get("count", 0) for o in eda_results["outliers"].values()) | |
| context_parts.append(f"Outliers flagged: {outlier_count}") | |
| if eda_results.get("correlation"): | |
| strong = [] | |
| corr = eda_results["correlation"] | |
| for c1 in corr: | |
| for c2, val in corr[c1].items(): | |
| if c1 != c2 and abs(val) > 0.7: | |
| strong.append(f"{c1}-{c2}: {val:.2f}") | |
| if strong: | |
| context_parts.append(f"Strong correlations: {', '.join(strong[:5])}") | |
| messages = [ | |
| { | |
| "role": "system", | |
| "content": ( | |
| "You are a senior business analyst. Generate 4-6 prioritized business recommendations.\n" | |
| "For each recommendation, output a JSON object with:\n" | |
| "- severity: 'critical', 'opportunity', or 'strength'\n" | |
| "- title: A concise action-oriented title (max 15 words)\n" | |
| "- description: A specific 1-2 sentence explanation with numbers\n\n" | |
| "Return ONLY a JSON array of these objects. No markdown, no extra text.\n" | |
| "Use 'critical' for urgent data issues, 'opportunity' for growth areas, 'strength' for positives." | |
| ) | |
| }, | |
| { | |
| "role": "user", | |
| "content": f"Dataset Analysis:\n{chr(10).join(context_parts)}\n\nGenerate 4-6 business recommendations." | |
| } | |
| ] | |
| result = _call_openai(messages, max_tokens=1200, temperature=0.6) | |
| if not result: | |
| return [] | |
| try: | |
| recs = json.loads(result) | |
| if isinstance(recs, list) and len(recs) > 0: | |
| # Validate structure | |
| valid = [] | |
| for r in recs[:6]: | |
| if isinstance(r, dict) and "title" in r: | |
| valid.append({ | |
| "severity": r.get("severity", "opportunity"), | |
| "title": r.get("title", ""), | |
| "description": r.get("description", "") | |
| }) | |
| return valid if valid else [] | |
| except (json.JSONDecodeError, TypeError): | |
| pass | |
| return [] | |
| def generate_key_insights(eda_summary: str, chart_descriptions: List[str]) -> List[str]: | |
| """Generate 5-8 key analytical insights from the full analysis.""" | |
| charts_text = "\n".join([f"- {desc}" for desc in chart_descriptions]) if chart_descriptions else "No charts available." | |
| messages = [ | |
| { | |
| "role": "system", | |
| "content": ( | |
| "You are a senior data analyst producing a key insights report. " | |
| "Generate exactly 6-8 bullet-point insights. Each insight must be:\n" | |
| "1. Specific (include numbers/percentages where possible)\n" | |
| "2. Actionable (suggest a business action)\n" | |
| "3. Clear and concise (1-2 sentences max per insight)\n\n" | |
| "Format: Return ONLY a JSON array of strings, e.g. [\"insight 1\", \"insight 2\", ...]\n" | |
| "Do NOT include any markdown formatting or extra text." | |
| ) | |
| }, | |
| { | |
| "role": "user", | |
| "content": ( | |
| f"EDA Summary:\n{eda_summary}\n\n" | |
| f"Charts Generated:\n{charts_text}\n\n" | |
| "Generate 6-8 key business insights." | |
| ) | |
| } | |
| ] | |
| result = _call_openai(messages, max_tokens=1200, temperature=0.6) | |
| if not result: | |
| return [ | |
| "Data quality improvements needed β missing values and duplicates were detected and handled.", | |
| "Review outlier records for potential data entry errors or genuinely extreme values.", | |
| "Explore the strongest correlations to understand key revenue drivers.", | |
| "Segment analysis reveals performance disparities β consider targeted strategies.", | |
| "Time-based trends suggest seasonal patterns worth investigating for planning.", | |
| "Top-performing categories should receive increased investment and focus." | |
| ] | |
| try: | |
| insights = json.loads(result) | |
| if isinstance(insights, list) and len(insights) > 0: | |
| return insights[:8] | |
| except (json.JSONDecodeError, TypeError): | |
| # Try to parse line by line | |
| lines = [line.strip().lstrip("β’-*0123456789.) ") for line in result.split("\n") if line.strip()] | |
| lines = [l for l in lines if len(l) > 20] | |
| if lines: | |
| return lines[:8] | |
| return [ | |
| "Data quality improvements needed β missing values and duplicates were detected and handled.", | |
| "Review outlier records for potential data entry errors or genuinely extreme values.", | |
| "Explore the strongest correlations to understand key revenue drivers.", | |
| "Segment analysis reveals performance disparities β consider targeted strategies.", | |
| "Time-based trends suggest seasonal patterns worth investigating for planning.", | |
| "Top-performing categories should receive increased investment and focus." | |
| ] | |
| def generate_forecast_commentary(forecast_summary: str) -> str: | |
| """Generate a narrative commentary on forecast results.""" | |
| messages = [ | |
| { | |
| "role": "system", | |
| "content": ( | |
| "You are a senior data analyst providing forecast commentary. " | |
| "Write a clear 3-4 sentence analysis of the forecast results. " | |
| "Include expected growth/decline percentage, key drivers, and a recommendation. " | |
| "Do NOT use markdown formatting. Be specific with numbers." | |
| ) | |
| }, | |
| { | |
| "role": "user", | |
| "content": f"Forecast Summary:\n{forecast_summary}\n\nProvide analytical commentary." | |
| } | |
| ] | |
| result = _call_openai(messages, max_tokens=400, temperature=0.6) | |
| if not result: | |
| return ( | |
| "The forecast model projects a continuation of current trends over the next 3 months. " | |
| "Consider monitoring key performance indicators closely and adjusting strategy based on " | |
| "actual vs. projected performance. External market factors may influence actual results." | |
| ) | |
| return result | |
| def build_dataset_summary(df_info: Dict[str, Any], eda_results: Dict[str, Any]) -> str: | |
| """Build a comprehensive text summary including full EDA cleaning results.""" | |
| lines = [] | |
| lines.append(f"Dataset: {df_info.get('rows', '?')} rows Γ {df_info.get('columns', '?')} columns") | |
| lines.append(f"Columns: {', '.join(df_info.get('column_names', []))}") | |
| # ββ EDA CLEANING SUMMARY βββββββββββββββββββββββββββββββββββββββββββββ | |
| lines.append("\n=== DATA CLEANING PERFORMED ===") | |
| # Duplicates | |
| dupes = eda_results.get("duplicates", {}) | |
| lines.append(f"Duplicates: {dupes.get('found', 0)} duplicate rows detected and removed. " | |
| f"{dupes.get('rows_after', '?')} rows remain after deduplication.") | |
| # Missing values | |
| mv = eda_results.get("missing_values", {}) | |
| total_before = mv.get("total_before", 0) | |
| total_after = mv.get("total_after", 0) | |
| lines.append(f"Missing Values: {total_before} nulls found before cleaning, " | |
| f"{total_after} remaining after filling.") | |
| strategies = mv.get("strategies", {}) | |
| if strategies: | |
| lines.append("Fill strategies used per column:") | |
| for col, strat in strategies.items(): | |
| lines.append(f" - {col}: filled using {strat}") | |
| # Type fixes | |
| type_fixes = eda_results.get("type_fixes", []) | |
| if type_fixes: | |
| lines.append(f"Data Type Fixes: {len(type_fixes)} columns corrected:") | |
| for fix in type_fixes: | |
| lines.append(f" - {fix['column']}: {fix['from']} β {fix['to']}") | |
| # Capitalisation | |
| cap = eda_results.get("capitalisation", {}) | |
| norm_cols = cap.get("normalised_columns", []) | |
| if norm_cols: | |
| lines.append(f"Text Normalisation: {len(norm_cols)} columns standardised " | |
| f"to Title Case: {', '.join(norm_cols)}") | |
| # Outliers | |
| outliers = eda_results.get("outliers", {}) | |
| if outliers: | |
| lines.append(f"Outliers: Detected in {len(outliers)} columns via IQR method " | |
| f"(flagged but NOT removed):") | |
| for col, info in outliers.items(): | |
| lines.append(f" - {col}: {info['count']} outliers " | |
| f"(valid range: {info['lower_bound']} to {info['upper_bound']})") | |
| else: | |
| lines.append("Outliers: None detected.") | |
| # ββ STATISTICS βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if eda_results.get("summary_stats"): | |
| lines.append("\n=== KEY STATISTICS ===") | |
| for col, stats in list(eda_results["summary_stats"].items())[:6]: | |
| lines.append(f" {col}: mean={stats.get('mean','?')}, " | |
| f"median={stats.get('median','?')}, " | |
| f"std={stats.get('std','?')}, " | |
| f"min={stats.get('min','?')}, " | |
| f"max={stats.get('max','?')}") | |
| # ββ CATEGORICAL INFO βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if eda_results.get("categorical_info"): | |
| lines.append("\n=== CATEGORICAL COLUMNS ===") | |
| for col, info in list(eda_results["categorical_info"].items())[:5]: | |
| top = list(info.get("top_values", {}).keys())[:3] | |
| lines.append(f" {col}: {info.get('unique_count','?')} unique values " | |
| f"(top: {', '.join(top)})") | |
| # ββ DATE INFO ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if eda_results.get("date_info"): | |
| lines.append("\n=== DATE RANGE ===") | |
| for col, info in eda_results["date_info"].items(): | |
| lines.append(f" {col}: {info.get('min','?')} to {info.get('max','?')} " | |
| f"({info.get('range_days','?')} days)") | |
| return "\n".join(lines) | |