132 lines
5.2 KiB
Python
132 lines
5.2 KiB
Python
"""Dashboard views: home, run list, run detail, run status."""
|
|
from django.shortcuts import render, get_object_or_404
|
|
from django.http import JsonResponse
|
|
|
|
from analysis.models import AnalysisRun
|
|
|
|
|
|
def dashboard(request):
|
|
"""Home page: recent runs overview."""
|
|
runs = AnalysisRun.objects.all()[:20]
|
|
return render(request, 'analysis/dashboard.html', {'runs': runs})
|
|
|
|
|
|
def run_list(request):
|
|
"""List all analysis runs with pagination and optional ?type= filter."""
|
|
run_type_filter = request.GET.get('type')
|
|
page = int(request.GET.get('page', 1))
|
|
per_page = 20
|
|
|
|
qs = AnalysisRun.objects.all()
|
|
if run_type_filter in dict(AnalysisRun.RUN_TYPE_CHOICES):
|
|
qs = qs.filter(run_type=run_type_filter)
|
|
|
|
total = qs.count()
|
|
runs = qs[(page - 1) * per_page: page * per_page]
|
|
return render(request, 'analysis/run_list.html', {
|
|
'runs': runs,
|
|
'page': page,
|
|
'total_pages': (total + per_page - 1) // per_page if total else 1,
|
|
'current_type': run_type_filter or '',
|
|
})
|
|
|
|
|
|
def run_detail(request, display_id):
|
|
"""Details for a single analysis run."""
|
|
import json
|
|
run = get_object_or_404(AnalysisRun, display_id=display_id)
|
|
clusters = run.clusters.all().order_by('-size')
|
|
|
|
# ── Global SVD features (compute on-demand from the stored run data) ──
|
|
svd_features = []
|
|
svd_explained_var = []
|
|
try:
|
|
from analysis.data_loader import load_csv_directory, load_from_db
|
|
import polars as pl
|
|
from sklearn.decomposition import TruncatedSVD
|
|
import numpy as np
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
lf = None
|
|
if run.sqlite_table:
|
|
lf = load_from_db(run.sqlite_table)
|
|
if lf is None and run.csv_glob:
|
|
lf, _, _, _, _ = load_csv_directory(run.csv_glob)
|
|
|
|
if lf is not None:
|
|
schema = lf.collect_schema()
|
|
numeric_types = (pl.Int8, pl.Int16, pl.Int32, pl.Int64,
|
|
pl.UInt8, pl.UInt16, pl.UInt32, pl.UInt64,
|
|
pl.Float32, pl.Float64)
|
|
num_cols = [n for n, dt in zip(schema.names(), schema.dtypes()) if dt in numeric_types and not n.startswith('_')]
|
|
|
|
if len(num_cols) >= 3:
|
|
n_rows_lf = lf.select(pl.len()).collect(streaming=True).item()
|
|
sample = min(int(n_rows_lf), 50000)
|
|
df = lf.sample(n=sample, seed=42).select(num_cols).collect(streaming=True) if n_rows_lf > sample else lf.select(num_cols).collect(streaming=True)
|
|
mat = np.nan_to_num(df.to_numpy(), nan=0.0)
|
|
from sklearn.preprocessing import StandardScaler
|
|
mat = StandardScaler().fit_transform(mat)
|
|
|
|
n_comp = min(20, min(mat.shape) - 1)
|
|
if n_comp >= 2:
|
|
svd = TruncatedSVD(n_components=n_comp, random_state=42)
|
|
svd.fit(mat)
|
|
var_ratio = svd.explained_variance_ratio_.tolist()
|
|
svd_explained_var = var_ratio
|
|
for comp_idx in range(min(10, n_comp)):
|
|
loadings = np.abs(svd.components_[comp_idx])
|
|
top_idx = np.argsort(loadings)[-5:][::-1]
|
|
features = [{'feature': num_cols[i], 'strength': round(float(loadings[i]), 4)} for i in top_idx]
|
|
svd_features.append({
|
|
'component': comp_idx + 1,
|
|
'explained_var': round(var_ratio[comp_idx] * 100, 2),
|
|
'cumulative_var': round(sum(var_ratio[:comp_idx + 1]) * 100, 2),
|
|
'top_features': features,
|
|
})
|
|
except Exception as exc:
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning(f'SVD feature extraction failed for run {run.display_id}: {exc}')
|
|
|
|
# ── LLM workflow timeline ──
|
|
llm_timeline_json = ''
|
|
if run.run_type == 'auto':
|
|
llm_thinking = run.llm_thinking or ''
|
|
tool_calls = run.tool_calls_json or []
|
|
if tool_calls or llm_thinking:
|
|
llm_timeline_json = json.dumps({
|
|
'thinking': llm_thinking,
|
|
'tool_calls': tool_calls,
|
|
})
|
|
|
|
return render(request, 'analysis/run_detail.html', {
|
|
'run': run,
|
|
'clusters': clusters,
|
|
'svd_features': svd_features,
|
|
'svd_explained_var': svd_explained_var,
|
|
'llm_timeline_json': llm_timeline_json,
|
|
})
|
|
|
|
|
|
def run_status_api(request, display_id):
|
|
"""Return current run status as JSON."""
|
|
run = get_object_or_404(AnalysisRun, display_id=display_id)
|
|
return JsonResponse({
|
|
'id': run.display_id,
|
|
'status': run.status,
|
|
'entity_count': run.entity_count,
|
|
'cluster_count': run.cluster_count,
|
|
'error_message': run.error_message,
|
|
'progress_pct': run.progress_pct,
|
|
'progress_msg': run.progress_msg,
|
|
'run_log': run.run_log[-2000:], # last 2000 chars
|
|
'llm_thinking': run.llm_thinking[-50000:] if run.llm_thinking else '',
|
|
'tool_calls': run.tool_calls_json,
|
|
})
|
|
|
|
|
|
def start_analysis(request):
|
|
"""Placeholder: API endpoint to trigger analysis via MCP."""
|
|
return JsonResponse({'status': 'not_implemented', 'message': 'Use MCP tools to trigger analysis'})
|