Pulastya B commited on
Commit
296ce82
·
1 Parent(s): 6badf55

Add file path removal, fix progress tracking, add Assets sidebar

Browse files
FIXES_SUMMARY.md DELETED
@@ -1,232 +0,0 @@
1
- # Fixes Summary - Model Metrics & UX Improvements
2
-
3
- ## Issues Fixed
4
-
5
- ### 1. ✅ Best Model Metrics Showing 0.0000 (HIGH PRIORITY)
6
-
7
- **Problem:**
8
- - Enhanced summary displayed `R² Score: 0.0000, RMSE: 0.0000, MAE: 0.0000`
9
- - Backend logs showed correct values: R²=0.713, RMSE=0.207
10
-
11
- **Root Cause:**
12
- The `_generate_enhanced_summary()` method in `src/orchestrator.py` was extracting metrics incorrectly:
13
- ```python
14
- best_model_data = models_data.get(best_model_name, {})
15
- metrics["best_model"] = {
16
- "r2_score": best_model_data.get("r2", 0), # ❌ Wrong! Metrics not at top level
17
- }
18
- ```
19
-
20
- The actual structure from `train_baseline_models` is:
21
- ```python
22
- {
23
- "models": {
24
- "xgboost": {
25
- "test_metrics": {
26
- "r2": 0.713,
27
- "rmse": 0.207,
28
- "mae": 0.15
29
- }
30
- }
31
- }
32
- }
33
- ```
34
-
35
- **Fix:**
36
- Updated lines 960-988 in `src/orchestrator.py`:
37
- ```python
38
- best_model_data = models_data.get(best_model_name, {})
39
- test_metrics = best_model_data.get("test_metrics", {}) # ✅ Access nested test_metrics
40
-
41
- metrics["best_model"] = {
42
- "name": best_model_name,
43
- "r2_score": test_metrics.get("r2", 0), # ✅ Now gets correct value
44
- "rmse": test_metrics.get("rmse", 0),
45
- "mae": test_metrics.get("mae", 0)
46
- }
47
- ```
48
-
49
- ---
50
-
51
- ### 2. ✅ Missing Baseline Model Comparison (HIGH PRIORITY)
52
-
53
- **Problem:**
54
- - Only showing final tuned XGBoost model
55
- - Not displaying comparison of all baseline models (Logistic Regression, Random Forest, XGBoost, etc.) before tuning
56
- - User couldn't see which baseline model performed best
57
-
58
- **Fix:**
59
- Enhanced summary formatting in `src/orchestrator.py` (lines 1088-1132):
60
-
61
- **Before:**
62
- ```
63
- ### 🏆 Best Model Performance
64
- - Model: xgboost
65
- - R² Score: 0.7130
66
- ```
67
-
68
- **After:**
69
- ```
70
- ### 🔬 Baseline Models Comparison
71
-
72
- 🏆 **Xgboost**: R²=0.7130, RMSE=0.2070, MAE=0.1500
73
- **Random Forest**: R²=0.6850, RMSE=0.2180, MAE=0.1620
74
- **Lightgbm**: R²=0.6720, RMSE=0.2250, MAE=0.1680
75
- **Ridge**: R²=0.5420, RMSE=0.2890, MAE=0.2150
76
- **Lasso**: R²=0.5230, RMSE=0.2950, MAE=0.2200
77
- **Catboost**: R²=0.4950, RMSE=0.3100, MAE=0.2320
78
-
79
- ### ⚙️ Hyperparameter Tuning Results
80
- - Model Type: xgboost
81
- - Optimized Score: 0.7150
82
- ```
83
-
84
- Now shows:
85
- - ✅ All baseline models sorted by R² score (descending)
86
- - ✅ Best model highlighted with 🏆 emoji
87
- - ✅ Clear comparison before showing tuned results
88
- - ✅ Separate sections for baseline vs tuned models
89
-
90
- ---
91
-
92
- ### 3. ✅ Poor Formatting with Ugly Code Blocks (MEDIUM PRIORITY)
93
-
94
- **Problem:**
95
- - LLM responses included file paths like `./outputs/data/cleaned.csv`
96
- - Markdown code blocks appearing in structured data
97
- - Messy formatting that wasn't aesthetic
98
-
99
- **Fix:**
100
- Strengthened system prompt in `src/orchestrator.py` (lines 408-418):
101
-
102
- ```python
103
- **CRITICAL: User Interface Integration & Response Formatting**
104
- - The user interface automatically displays clickable buttons for all generated plots, reports, and outputs
105
- - **NEVER mention file paths** (e.g., "./outputs/plots/...", "./outputs/data/...", etc.) in your responses
106
- - **NEVER use markdown code blocks** for file paths or structured data in final summaries
107
- - DO NOT say "Output File: ..." or "Saved to: ..." - users can click buttons to view outputs
108
- - Simply describe what was created and what insights it shows
109
- - Use clean, aesthetic formatting with proper sections, bullet points, and spacing
110
- ```
111
-
112
- **Changes:**
113
- - ❌ Removed: "Output File: `./outputs/plots/heatmap.html`"
114
- - ✅ Replaced with: "Generated an interactive correlation heatmap showing relationships between variables"
115
- - ❌ Removed: "Saved cleaned data to: `./outputs/data/cleaned.csv`"
116
- - ✅ Replaced with: "Cleaned the dataset by handling missing values and outliers"
117
-
118
- ---
119
-
120
- ### 4. ✅ No Progress Indicators (MEDIUM PRIORITY)
121
-
122
- **Problem:**
123
- - Long-running workflows had no visibility for users
124
- - Users couldn't see which step the agent was on
125
- - No way to know if the system was stuck or processing
126
-
127
- **Fix:**
128
-
129
- **Backend (`src/orchestrator.py`):**
130
- 1. Added `progress_callback` parameter to `__init__` (lines 137-159)
131
- 2. Updated `_execute_tool()` to report progress (lines 1194-1200):
132
- ```python
133
- # Report progress before executing
134
- if self.progress_callback:
135
- self.progress_callback(tool_name, "running")
136
-
137
- # ... execute tool ...
138
-
139
- # Report completion
140
- if self.progress_callback:
141
- self.progress_callback(tool_name, "completed")
142
- ```
143
-
144
- **API (`src/api/app.py`):**
145
- 1. Added global `progress_store` dict (line 45)
146
- 2. Created `/api/progress/{session_id}` endpoint (lines 88-93)
147
- 3. Updated `/run` endpoint to track progress (lines 244-258):
148
- ```python
149
- def progress_callback(tool_name: str, status: str):
150
- progress_store[session_key].append({
151
- "tool": tool_name,
152
- "status": status,
153
- "timestamp": time.time()
154
- })
155
- ```
156
- 4. Return progress in response (line 296)
157
-
158
- **Frontend (`FRRONTEEEND/components/ChatInterface.tsx`):**
159
- 1. Added `currentStep` state (line 48)
160
- 2. Display progress in typing indicator (lines 531-555):
161
- ```tsx
162
- {currentStep ? (
163
- <div className="flex items-center gap-3">
164
- <div className="flex gap-1">
165
- <span className="w-1.5 h-1.5 bg-emerald-500 rounded-full animate-bounce"></span>
166
- </div>
167
- <span className="text-sm text-white/60">
168
- 🔧 {currentStep.replace(/_/g, ' ').replace('train', 'Training')...}
169
- </span>
170
- </div>
171
- ) : (
172
- // Default loading animation
173
- )}
174
- ```
175
-
176
- **Result:**
177
- - ✅ User sees: "🔧 Training Baseline Models..." while models train
178
- - ✅ User sees: "🔧 Cleaning Missing Values..." during data cleaning
179
- - ✅ User sees: "🔧 Generating Plotly Dashboard..." during visualization
180
- - ✅ Clear visibility of current step throughout workflow
181
- - ✅ Emerald-colored animated dots indicate active processing
182
-
183
- ---
184
-
185
- ## Testing Recommendations
186
-
187
- 1. **Metric Extraction:**
188
- - Upload earthquake dataset
189
- - Run full ML pipeline
190
- - Verify metrics display correctly (not 0.0000)
191
-
192
- 2. **Baseline Comparison:**
193
- - Check that all models appear in summary
194
- - Verify sorting by R² score
195
- - Confirm best model has 🏆 emoji
196
-
197
- 3. **Formatting:**
198
- - Check that no file paths appear in responses
199
- - Verify clean markdown without code blocks for structured data
200
-
201
- 4. **Progress Indicators:**
202
- - Upload large dataset
203
- - Watch for step-by-step progress updates
204
- - Confirm smooth transition when complete
205
-
206
- ## Files Modified
207
-
208
- 1. `src/orchestrator.py` (4 changes)
209
- - Lines 137-159: Added `progress_callback` parameter
210
- - Lines 960-988: Fixed metric extraction from `test_metrics`
211
- - Lines 1088-1132: Added baseline model comparison section
212
- - Lines 408-418: Strengthened formatting rules
213
- - Lines 1194-1200, 1248-1258: Added progress reporting
214
-
215
- 2. `src/api/app.py` (4 changes)
216
- - Line 7: Import `time`
217
- - Line 45: Added `progress_store` dict
218
- - Lines 88-93: Created `/api/progress/{session_id}` endpoint
219
- - Lines 170-185, 244-258, 296: Integrated progress callback
220
-
221
- 3. `FRRONTEEEND/components/ChatInterface.tsx` (3 changes)
222
- - Line 48: Added `currentStep` state
223
- - Line 140: Clear progress on response
224
- - Lines 531-555: Enhanced typing indicator with progress display
225
-
226
- ## Impact
227
-
228
- - ✅ Model metrics now display correctly (not 0.0000)
229
- - ✅ Users can see all baseline models before tuning results
230
- - ✅ Responses are cleaner without file paths/ugly code blocks
231
- - ✅ Real-time progress visibility improves UX significantly
232
- - ✅ Users won't think the system is stuck during long operations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
FRRONTEEEND/components/ChatInterface.tsx CHANGED
@@ -1,7 +1,7 @@
1
 
2
  import React, { useState, useRef, useEffect } from 'react';
3
  import { motion, AnimatePresence } from 'framer-motion';
4
- import { Send, Plus, Search, Settings, MoreHorizontal, User, Bot, ArrowLeft, Paperclip, Sparkles, Trash2, X, Upload } from 'lucide-react';
5
  import { cn } from '../lib/utils';
6
  import { Logo } from './Logo';
7
  import ReactMarkdown from 'react-markdown';
@@ -48,6 +48,7 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
48
  const [currentStep, setCurrentStep] = useState<string>('');
49
  const [uploadedFile, setUploadedFile] = useState<File | null>(null);
50
  const [reportModalUrl, setReportModalUrl] = useState<string | null>(null);
 
51
  const fileInputRef = useRef<HTMLInputElement>(null);
52
  const scrollRef = useRef<HTMLDivElement>(null);
53
 
@@ -120,6 +121,7 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
120
  if (uploadedFile) {
121
  formData.append('file', uploadedFile);
122
  formData.append('task_description', input || 'Analyze this dataset and provide insights');
 
123
  } else if (hasRecentFile) {
124
  // For follow-up questions, extract the filename from recent context
125
  const fileNameMatch = recentFileMessage?.content.match(/Uploaded: (.+)/);
@@ -127,7 +129,7 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
127
 
128
  // Send follow-up request as a new task description
129
  formData.append('task_description', input);
130
- formData.append('session_id', activeSessionId);
131
 
132
  // Note: Backend needs to support session-based file context
133
  // For now, just send the task which should work with session memory
@@ -426,6 +428,15 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
426
  </div>
427
  </div>
428
  <div className="flex items-center gap-3">
 
 
 
 
 
 
 
 
 
429
  <button className="p-2 text-white/40 hover:text-white transition-colors">
430
  <Search className="w-5 h-5" />
431
  </button>
@@ -659,6 +670,138 @@ export const ChatInterface: React.FC<{ onBack: () => void }> = ({ onBack }) => {
659
  </div>
660
  </main>
661
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
  {/* Report Modal */}
663
  <AnimatePresence>
664
  {reportModalUrl && (
 
1
 
2
  import React, { useState, useRef, useEffect } from 'react';
3
  import { motion, AnimatePresence } from 'framer-motion';
4
+ import { Send, Plus, Search, Settings, MoreHorizontal, User, Bot, ArrowLeft, Paperclip, Sparkles, Trash2, X, Upload, Package, FileText, BarChart3, ChevronRight } from 'lucide-react';
5
  import { cn } from '../lib/utils';
6
  import { Logo } from './Logo';
7
  import ReactMarkdown from 'react-markdown';
 
48
  const [currentStep, setCurrentStep] = useState<string>('');
49
  const [uploadedFile, setUploadedFile] = useState<File | null>(null);
50
  const [reportModalUrl, setReportModalUrl] = useState<string | null>(null);
51
+ const [showAssets, setShowAssets] = useState(false);
52
  const fileInputRef = useRef<HTMLInputElement>(null);
53
  const scrollRef = useRef<HTMLDivElement>(null);
54
 
 
121
  if (uploadedFile) {
122
  formData.append('file', uploadedFile);
123
  formData.append('task_description', input || 'Analyze this dataset and provide insights');
124
+ formData.append('session_id', sessionKey); // Add session_id for progress tracking
125
  } else if (hasRecentFile) {
126
  // For follow-up questions, extract the filename from recent context
127
  const fileNameMatch = recentFileMessage?.content.match(/Uploaded: (.+)/);
 
129
 
130
  // Send follow-up request as a new task description
131
  formData.append('task_description', input);
132
+ formData.append('session_id', sessionKey); // Use same session key
133
 
134
  // Note: Backend needs to support session-based file context
135
  // For now, just send the task which should work with session memory
 
428
  </div>
429
  </div>
430
  <div className="flex items-center gap-3">
431
+ <button
432
+ onClick={() => setShowAssets(!showAssets)}
433
+ className={cn(
434
+ "p-2 transition-colors rounded-lg",
435
+ showAssets ? "text-emerald-400 bg-emerald-500/10" : "text-white/40 hover:text-white"
436
+ )}
437
+ >
438
+ <Package className="w-5 h-5" />
439
+ </button>
440
  <button className="p-2 text-white/40 hover:text-white transition-colors">
441
  <Search className="w-5 h-5" />
442
  </button>
 
670
  </div>
671
  </main>
672
 
673
+ {/* Assets Sidebar */}
674
+ <AnimatePresence>
675
+ {showAssets && (
676
+ <motion.aside
677
+ initial={{ x: 320, opacity: 0 }}
678
+ animate={{ x: 0, opacity: 1 }}
679
+ exit={{ x: 320, opacity: 0 }}
680
+ transition={{ type: "spring", damping: 25, stiffness: 200 }}
681
+ className="w-[320px] border-l border-white/5 bg-[#0a0a0a]/95 backdrop-blur-xl flex flex-col"
682
+ >
683
+ <div className="p-4 border-b border-white/5 flex items-center justify-between">
684
+ <div className="flex items-center gap-2">
685
+ <Package className="w-5 h-5 text-emerald-400" />
686
+ <h3 className="font-bold text-sm">Assets</h3>
687
+ </div>
688
+ <button
689
+ onClick={() => setShowAssets(false)}
690
+ className="p-1.5 rounded-lg hover:bg-white/5 transition-colors"
691
+ >
692
+ <X className="w-4 h-4" />
693
+ </button>
694
+ </div>
695
+
696
+ <div className="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar">
697
+ {/* Collect all assets from messages */}
698
+ {(() => {
699
+ const allPlots: Array<{title: string, url: string, type?: string}> = [];
700
+ const allReports: Array<{name: string, path: string}> = [];
701
+ const allModels: string[] = [];
702
+
703
+ activeSession.messages.forEach(msg => {
704
+ if (msg.plots) allPlots.push(...msg.plots);
705
+ if (msg.reports) allReports.push(...msg.reports);
706
+ // Extract model references from content
707
+ if (msg.content.includes('xgboost') || msg.content.includes('model')) {
708
+ const modelMatch = msg.content.match(/\b(xgboost|random_forest|catboost|lightgbm)[^\s]*/gi);
709
+ if (modelMatch) allModels.push(...modelMatch);
710
+ }
711
+ });
712
+
713
+ const uniqueModels = [...new Set(allModels)];
714
+
715
+ return (
716
+ <>
717
+ {/* Models Section */}
718
+ {uniqueModels.length > 0 && (
719
+ <div>
720
+ <div className="flex items-center gap-2 mb-3">
721
+ <FileText className="w-4 h-4 text-blue-400" />
722
+ <h4 className="text-xs font-bold uppercase tracking-wider text-white/60">Models ({uniqueModels.length})</h4>
723
+ </div>
724
+ <div className="space-y-2">
725
+ {uniqueModels.slice(0, 5).map((model, idx) => (
726
+ <div
727
+ key={idx}
728
+ className="p-3 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10 transition-all cursor-pointer group"
729
+ >
730
+ <div className="flex items-center justify-between">
731
+ <span className="text-sm text-white/80 truncate flex-1">{model}</span>
732
+ <ChevronRight className="w-4 h-4 text-white/40 group-hover:text-white/80 transition-all" />
733
+ </div>
734
+ </div>
735
+ ))}
736
+ </div>
737
+ </div>
738
+ )}
739
+
740
+ {/* Plots Section */}
741
+ {allPlots.length > 0 && (
742
+ <div>
743
+ <div className="flex items-center gap-2 mb-3">
744
+ <BarChart3 className="w-4 h-4 text-emerald-400" />
745
+ <h4 className="text-xs font-bold uppercase tracking-wider text-white/60">Visualizations ({allPlots.length})</h4>
746
+ </div>
747
+ <div className="space-y-2">
748
+ {allPlots.map((plot, idx) => (
749
+ <button
750
+ key={idx}
751
+ onClick={() => setReportModalUrl(plot.url)}
752
+ className="w-full p-3 rounded-lg bg-white/5 border border-white/10 hover:bg-emerald-500/10 hover:border-emerald-500/30 transition-all text-left group"
753
+ >
754
+ <div className="flex items-center justify-between">
755
+ <span className="text-sm text-white/80 truncate flex-1">{plot.title}</span>
756
+ <ChevronRight className="w-4 h-4 text-white/40 group-hover:text-emerald-400 transition-all" />
757
+ </div>
758
+ <span className="text-xs text-white/40 mt-1 block">{plot.type || 'interactive'}</span>
759
+ </button>
760
+ ))}
761
+ </div>
762
+ </div>
763
+ )}
764
+
765
+ {/* Reports Section */}
766
+ {allReports.length > 0 && (
767
+ <div>
768
+ <div className="flex items-center gap-2 mb-3">
769
+ <FileText className="w-4 h-4 text-purple-400" />
770
+ <h4 className="text-xs font-bold uppercase tracking-wider text-white/60">Reports ({allReports.length})</h4>
771
+ </div>
772
+ <div className="space-y-2">
773
+ {allReports.map((report, idx) => (
774
+ <button
775
+ key={idx}
776
+ onClick={() => setReportModalUrl(report.path)}
777
+ className="w-full p-3 rounded-lg bg-white/5 border border-white/10 hover:bg-purple-500/10 hover:border-purple-500/30 transition-all text-left group"
778
+ >
779
+ <div className="flex items-center justify-between">
780
+ <span className="text-sm text-white/80 truncate flex-1">{report.name}</span>
781
+ <ChevronRight className="w-4 h-4 text-white/40 group-hover:text-purple-400 transition-all" />
782
+ </div>
783
+ </button>
784
+ ))}
785
+ </div>
786
+ </div>
787
+ )}
788
+
789
+ {/* Empty State */}
790
+ {allPlots.length === 0 && allReports.length === 0 && uniqueModels.length === 0 && (
791
+ <div className="flex flex-col items-center justify-center h-full text-center p-8">
792
+ <Package className="w-12 h-12 text-white/10 mb-3" />
793
+ <p className="text-sm text-white/40 mb-1">No assets yet</p>
794
+ <p className="text-xs text-white/30">Upload a dataset to generate visualizations and models</p>
795
+ </div>
796
+ )}
797
+ </>
798
+ );
799
+ })()}
800
+ </div>
801
+ </motion.aside>
802
+ )}
803
+ </AnimatePresence>
804
+
805
  {/* Report Modal */}
806
  <AnimatePresence>
807
  {reportModalUrl && (
src/orchestrator.py CHANGED
@@ -2304,8 +2304,26 @@ You are a DOER. Complete workflows based on user intent."""
2304
  task_description
2305
  )
2306
  summary_text = enhanced_summary["text"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2307
  metrics_data = enhanced_summary.get("metrics", {})
2308
  artifacts_data = enhanced_summary.get("artifacts", {})
 
2309
  plots_data = enhanced_summary.get("plots", [])
2310
  print(f"✅ Enhanced summary generated with {len(plots_data)} plots, {len(metrics_data)} metrics")
2311
  except Exception as e:
 
2304
  task_description
2305
  )
2306
  summary_text = enhanced_summary["text"]
2307
+
2308
+ # 🧹 POST-PROCESS: Remove any file paths that slipped through
2309
+ import re
2310
+ # Remove file path patterns
2311
+ summary_text = re.sub(r'\./outputs/[^\s\)]+', '[generated file]', summary_text)
2312
+ summary_text = re.sub(r'/outputs/[^\s\)]+', '[generated file]', summary_text)
2313
+ summary_text = re.sub(r'outputs/[^\s\)]+', '[generated file]', summary_text)
2314
+ # Remove common file path mentions
2315
+ summary_text = re.sub(r'saved to:?\s*[^\s]+', 'generated', summary_text, flags=re.IGNORECASE)
2316
+ summary_text = re.sub(r'output file:?\s*[^\s]+', 'output generated', summary_text, flags=re.IGNORECASE)
2317
+ summary_text = re.sub(r'file path:?\s*[^\s]+', 'file generated', summary_text, flags=re.IGNORECASE)
2318
+ # Remove filename patterns in parentheses and backticks
2319
+ summary_text = re.sub(r'\([^\)]*\.(csv|pkl|html|png|json)[^\)]*\)', '', summary_text)
2320
+ summary_text = re.sub(r'`[^`]*\.(csv|pkl|html|png|json)[^`]*`', '', summary_text)
2321
+ # Clean up table separators that mention paths
2322
+ summary_text = re.sub(r'\|\s*[^\|]*\.(csv|pkl|html|png)[^\|]*\s*\|', '| [see artifacts] |', summary_text)
2323
+
2324
  metrics_data = enhanced_summary.get("metrics", {})
2325
  artifacts_data = enhanced_summary.get("artifacts", {})
2326
+ artifacts_data = enhanced_summary.get("artifacts", {})
2327
  plots_data = enhanced_summary.get("plots", [])
2328
  print(f"✅ Enhanced summary generated with {len(plots_data)} plots, {len(metrics_data)} metrics")
2329
  except Exception as e: