Spaces:
Sleeping
Sleeping
File size: 19,363 Bytes
6193995 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 | """
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)
|