Datavision / backend /agents /smart_chart.py
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
7.18 kB
"""
PURE LLM-DRIVEN VISUALIZATION - ZERO HARDCODING
=================================================
This is the ULTIMATE autonomous chart system.
The LLM generates the COMPLETE Plotly configuration.
NO predefined chart types.
NO hardcoded generators.
LLM decides EVERYTHING based on:
1. User's query
2. User's actual data
3. What makes sense for the visualization
Just like ChatGPT - pure intelligence, pure autonomy.
"""
import json
import logging
from typing import Optional
import pandas as pd
from core.llm import chat
logger = logging.getLogger(__name__)
def generate_visualization_pure_llm(query: str, df: pd.DataFrame) -> Optional[str]:
"""
100% LLM-DRIVEN VISUALIZATION.
The LLM decides:
- IF a chart should be generated
- WHAT type of chart
- WHICH columns to use
- HOW to structure the data
- ALL styling and configuration
Returns complete Plotly JSON - no hardcoding!
"""
if df is None or df.empty:
return None
try:
# Prepare data context for LLM
columns = list(df.columns)
dtypes = {col: str(df[col].dtype) for col in columns}
sample_data = df.head(10).to_dict(orient='records')
row_count = len(df)
# For numeric columns, get basic stats
numeric_summary = {}
for col in df.select_dtypes(include=['int64', 'float64']).columns:
numeric_summary[col] = {
"min": float(df[col].min()),
"max": float(df[col].max()),
"sum": float(df[col].sum()),
"mean": float(df[col].mean())
}
# For categorical columns, get value counts
categorical_summary = {}
for col in df.select_dtypes(include=['object']).columns[:5]:
categorical_summary[col] = df[col].value_counts().head(10).to_dict()
# THE MASTER PROMPT - LLM does EVERYTHING
prompt = f"""You are an expert data visualization AI. Generate a Plotly chart configuration.
USER REQUEST: "{query}"
DATA AVAILABLE:
- Columns: {columns}
- Data Types: {dtypes}
- Total Rows: {row_count}
- Sample Data: {json.dumps(sample_data[:5], default=str)[:1000]}
NUMERIC DATA SUMMARY:
{json.dumps(numeric_summary, indent=2)[:800]}
CATEGORICAL DATA SUMMARY:
{json.dumps(categorical_summary, default=str, indent=2)[:800]}
YOUR TASK:
1. Analyze what the user is asking for
2. Decide the BEST visualization (or no chart if not needed)
3. Generate a COMPLETE Plotly chart configuration using the ACTUAL data values above
IMPORTANT RULES:
- Use ONLY the actual data values shown above
- Choose the visualization that best answers the user's question
- Include proper titles, labels, and styling
- Use dark theme colors (paper_bgcolor: "rgba(0,0,0,0)", font color: "#9ca3af")
- Accent colors: teal (#14b8a6), indigo (#6366f1), amber (#f59e0b)
- For pie/donut charts, use "labels" and "values" (NEVER "x" and "y") in the data array.
Return your response in this EXACT format:
{{
"should_generate": true or false,
"visualization_type": "what you chose and why",
"plotly_chart": {{COMPLETE PLOTLY JSON HERE}}
}}
If no chart is needed, return:
{{"should_generate": false, "reason": "why no chart needed"}}
Generate the visualization:"""
response = chat(prompt, temperature=0.2, max_tokens=2000)
# Parse the response
response = response.strip()
# Find JSON in response
if '```json' in response:
response = response.split('```json')[1].split('```')[0]
elif '```' in response:
response = response.split('```')[1].split('```')[0]
# Find the JSON object
start = response.find('{')
end = response.rfind('}') + 1
if start >= 0 and end > start:
response = response[start:end]
result = json.loads(response)
# Check if we should generate
if not result.get("should_generate", True):
logger.info(f"LLM decided not to generate chart: {result.get('reason', 'No reason')}")
return None
# Get the Plotly chart configuration
plotly_config = result.get("plotly_chart", {})
if not plotly_config or "data" not in plotly_config:
logger.warning("LLM returned invalid Plotly config")
return None
# Ensure dark theme styling
if "layout" not in plotly_config:
plotly_config["layout"] = {}
plotly_config["layout"]["paper_bgcolor"] = "rgba(0,0,0,0)"
plotly_config["layout"]["plot_bgcolor"] = "rgba(0,0,0,0)"
plotly_config["layout"]["font"] = {"color": "#9ca3af"}
if "title" in plotly_config["layout"]:
if isinstance(plotly_config["layout"]["title"], str):
plotly_config["layout"]["title"] = {
"text": plotly_config["layout"]["title"],
"font": {"color": "#fff"}
}
elif isinstance(plotly_config["layout"]["title"], dict):
plotly_config["layout"]["title"]["font"] = {"color": "#fff"}
logger.info(f"LLM generated {result.get('visualization_type', 'chart')}")
return f"\n\n```plotly_chart\n{json.dumps(plotly_config)}\n```"
except json.JSONDecodeError as e:
logger.error(f"Failed to parse LLM response as JSON: {e}")
return None
except Exception as e:
logger.error(f"Pure LLM visualization error: {e}")
return None
def should_visualize(query: str) -> bool:
"""
Quick check if query likely needs visualization.
This is just a hint - LLM makes final decision.
"""
viz_hints = [
'chart', 'graph', 'plot', 'visualize', 'show', 'display',
'pie', 'bar', 'line', 'scatter', 'distribution',
'top', 'bottom', 'compare', 'trend', 'breakdown',
'heatmap', 'histogram', 'funnel', 'gauge', 'mindmap',
'how many', 'what is the total', 'percentage',
'img', 'image', 'picture', 'dashboard', 'report'
]
query_lower = query.lower()
return any(hint in query_lower for hint in viz_hints)
def generate_smart_chart(query: str, df: pd.DataFrame) -> Optional[str]:
"""
Main entry point for smart chart generation.
This is 100% autonomous:
1. Checks if visualization might be needed
2. Calls LLM to generate complete chart
3. Returns Plotly JSON or None
"""
# Quick check for visualization hints
if not should_visualize(query):
# Still let LLM decide - it might choose to visualize anyway
pass
return generate_visualization_pure_llm(query, df)
# Backwards compatibility aliases
def smart_chart(query: str, df: pd.DataFrame) -> Optional[str]:
"""Alias for generate_smart_chart for backwards compatibility"""
return generate_smart_chart(query, df)
def get_color_palette_from_query(query: str) -> list:
"""Get color palette - returns default teal/indigo theme"""
return ["#14b8a6", "#6366f1", "#f59e0b", "#ef4444", "#8b5cf6", "#06b6d4", "#84cc16", "#f97316"]