import pandas as pd import json import traceback from typing import List, Dict # Try to import the existing LLM chat function, if it exists in Datavision try: from backend.utils.llm import chat except ImportError: # Fallback to importing from core if that's where it is try: from core.llm import chat except ImportError: # We will assume 'chat' is globally available in real_dashboard or we mock it def chat(prompt, **kwargs): return "" _schema_cache = {} def build_dashboard_schema_agent(df: pd.DataFrame, domain: str, chat_func=None) -> Dict: """ True AI Agent that designs the entire dashboard schema via LLM. Returns a list of ~24 chart specifications. """ global _schema_cache # Fast cache return to support real-time data slicing/filtering cache_key = f"{domain}_{','.join(sorted(df.columns))}" if cache_key in _schema_cache: print("⚡ Skipping LLM Architect: Returning cached dashboard schema for filtered dataset.") return _schema_cache[cache_key] if chat_func is None: try: # We import the chat func from real_dashboard dynamically to avoid circular imports from core.real_dashboard import chat as chat_func except: pass # Only provide chartable business fields to the architect. IDs remain in the # data grid, but must never become a visual axis. from core.real_dashboard import dashboard_column_profile profile = dashboard_column_profile(df) num_cols = profile['metrics'] cat_cols = profile['dimensions'] date_cols = profile['dates'] columns = num_cols + cat_cols + date_cols dtypes = {col: str(df[col].dtype) for col in columns} stats = {} for col in num_cols[:5]: # Limit to avoid huge prompt stats[col] = { "min": float(df[col].min()) if not pd.isna(df[col].min()) else 0, "max": float(df[col].max()) if not pd.isna(df[col].max()) else 0, "mean": float(df[col].mean()) if not pd.isna(df[col].mean()) else 0 } for col in cat_cols[:3]: stats[col] = { "unique_count": int(df[col].nunique()), "top_values": df[col].value_counts().head(3).index.tolist() } prompt = f""" You are an expert Power BI / Tableau Dashboard Architect. Your task is to design a useful executive dashboard with 8-12 distinct business charts for a {domain} dataset. Dataset Schema: Columns: {columns} Data Types: {dtypes} Sample Stats: {stats} Available Chart Types: "stacked_bar", "grouped_bar", "line", "area", "horizontal_bar", "pie", "donut", "scatter", "bubble", "treemap", "heatmap", "waterfall", "funnel", "sunburst", "radar", "3d_scatter", "3d_surface", "3d_bubble", "polar_bar", "sankey", "choropleth", "violin", "bullet", "density_contour", "parallel_categories", "multi_sunburst", "hexbin" CRITICAL INSTRUCTIONS: 1. Generate 8-12 charts only when the dataset supports them. Do not pad the dashboard with weak visuals. 2. Use a variety of chart types when they fit: trends for date fields, rankings and composition for low-cardinality categories, distributions for measures, and scatter/correlation for related measures. 3. The `x_column` and `y_column` MUST be EXACT strings from the Columns list provided above. You MUST provide BOTH x_column and y_column for EVERY chart. 4. For pie, donut, treemap, funnel, and waterfall: `x_column` is the category (Labels), and `y_column` is the numeric field to aggregate (Values). 5. For sankey and parallel_categories: `x_column` is Source Category, `y_column` is Target Category, and `color_by_column` is Value. 6. For 3d_scatter, 3d_surface, 3d_bubble: `x_column` is X, `y_column` is Y, and `color_by_column` is Z. 7. For choropleth: `x_column` MUST be a geographic location column (Country, State, City), `y_column` is the numeric value. 8. Ensure every chart uses logical column mappings based on the Data Types. 9. Architect a complete "Silicon Valley Premium" 'theme' for this dashboard based on the domain. Provide rich CSS gradients and glassmorphic colors. - `bg_gradient`: e.g., "linear-gradient(135deg, #0b1120 0%, #151e32 100%)" - `bg_pattern`: Select ONE of these 12 patterns based on domain: "grid", "dots", "mesh", "waves", "topography", "particles", "circuit", "honeycomb", "stripes", "boxes", "radial", "aurora" - `card_bg`: e.g., "rgba(20, 30, 50, 0.6)" (use rgba for glassmorphism) - `border_color`: e.g., "rgba(255,255,255,0.05)" - `text_primary` and `text_secondary`: Hex colors for text. 10. Respond ONLY with a valid JSON object containing exactly `theme` and `charts`. No markdown, no explanation. JSON Format required: {{ "theme": {{ "bg_gradient": "linear-gradient(to right, #000000, #1a1a1a)", "bg_pattern": "mesh", "card_bg": "rgba(25, 25, 25, 0.7)", "border_color": "rgba(255, 255, 255, 0.1)", "text_primary": "#ffffff", "text_secondary": "#a0a0a0", "chart_palette": ["#00ff00", "#ff0000", "#0000ff", "#ffff00", "#00ffff"] }}, "charts": [ {{ "title": "Clear Business Title", "chart_type": "stacked_bar", "x_column": "exact_column_name_from_list", "y_column": "exact_column_name_from_list", "color_by_column": "exact_column_name_or_empty_string", "aggregation": "sum|mean|count" }} ] }} """ try: if chat_func: response = chat_func(prompt, temperature=0.3, max_tokens=2500) else: from core.real_dashboard import chat as fallback_chat response = fallback_chat(prompt, temperature=0.3, max_tokens=2500) response = response.strip() if response.startswith('```json'): response = response[7:-3].strip() elif response.startswith('```'): response = response[3:-3].strip() schema = json.loads(response.strip()) if isinstance(schema, dict) and "charts" in schema: _schema_cache[cache_key] = schema return schema elif isinstance(schema, list) and len(schema) > 0: # Handle legacy case where LLM just returns list final_schema = { "theme": { "bg_gradient": "linear-gradient(135deg, #0f172a 0%, #1e293b 100%)", "bg_pattern": "mesh", "card_bg": "rgba(30, 41, 59, 0.7)", "border_color": "rgba(255, 255, 255, 0.1)", "text_primary": "#f8fafc", "text_secondary": "#94a3b8", "chart_palette": ["#38bdf8", "#818cf8", "#c084fc", "#e879f9", "#22d3ee"] }, "charts": schema } _schema_cache[cache_key] = final_schema return final_schema except Exception as e: print(f"AI Schema Agent failed: {e}") traceback.print_exc() # Deterministic business-aware fallback for unavailable AI providers. fallback_schema = [] primary_metric = num_cols[0] if num_cols else None if primary_metric: for date_col in date_cols[:1]: fallback_schema.append({"title": f"{primary_metric} Trend", "chart_type": "line", "x_column": date_col, "y_column": primary_metric, "color_by_column": "", "aggregation": "sum"}) for index, cat in enumerate(cat_cols[:3]): chart_type = ["horizontal_bar", "donut", "treemap"][index] fallback_schema.append({"title": f"{primary_metric} by {cat}", "chart_type": chart_type, "x_column": cat, "y_column": primary_metric, "color_by_column": "", "aggregation": "sum"}) if len(cat_cols) >= 2: fallback_schema.append({"title": f"{primary_metric} by {cat_cols[0]} and {cat_cols[1]}", "chart_type": "stacked_bar", "x_column": cat_cols[0], "y_column": primary_metric, "color_by_column": cat_cols[1], "aggregation": "sum"}) if len(num_cols) >= 2: fallback_schema.append({"title": f"{num_cols[0]} vs {num_cols[1]}", "chart_type": "scatter", "x_column": num_cols[0], "y_column": num_cols[1], "color_by_column": cat_cols[0] if cat_cols else "", "aggregation": "mean"}) final_schema = { "theme": { "background_color": "#0b1120", "card_background": "#151e32", "border_color": "#2a3441", "text_color": "#f8fafc", "text_secondary": "#e2e8f0", "chart_palette": ["#38bdf8", "#818cf8", "#c084fc", "#e879f9", "#22d3ee"] }, "charts": fallback_schema } _schema_cache[cache_key] = final_schema return final_schema