Files
tianxuan/analysis/views/manual.py
T
PM-pinou 66f68a2062 refactor(services): extract clustering pipeline from views/ to analysis/services/
Move _run_clustering_pipeline (~295 lines) from analysis/views/clustering.py
into analysis/services/clustering.py as three clean functions:
- run_clustering_pipeline: main pipeline orchestration
- compute_umap_embedding: UMAP 3D→2D fallback computation
- save_entity_profiles: persist cluster labels + UMAP coords to ORM

Views/clustering.py now delegates via thin wrapper. Callers (auto.py,
manual.py, run_pipeline.py) import directly from services.clustering.
2026-07-24 13:27:11 +08:00

192 lines
7.7 KiB
Python

"""Manual analysis page: workflow builder and run execution."""
import json
import threading
import traceback
import logging
from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from analysis.models import AnalysisRun
from .pipeline import _run_pipeline_worker
from analysis.services.clustering import run_clustering_pipeline
logger = logging.getLogger(__name__)
def manual_page(request):
"""Workflow builder: select dataset, add MCP tool steps, execute."""
from analysis.session_store import SessionStore
from analysis.tool_registry import get_tools_meta
import json as _json
# Get all tools metadata
tools = get_tools_meta()
core_names = {'profile_data', 'build_entity_profiles', 'compute_scores',
'run_clustering', 'extract_features', 'detect_anomalies',
'visualize_anomalies'}
diag_names = {'validate_data', 'explore_distributions', 'find_outliers',
'diagnose_clustering', 'compare_datasets', 'export_debug_sample',
'repair_schema'}
analysis_names = {'analyze_patterns', 'analyze_temporal', 'analyze_tls_health',
'analyze_geo_distribution', 'analyze_entity_detail'}
core_tools, diag_tools, analysis_tools, other_tools = [], [], [], []
for t in tools:
if t.name in core_names: core_tools.append(t)
elif t.name in diag_names: diag_tools.append(t)
elif t.name in analysis_names: analysis_tools.append(t)
else: other_tools.append(t)
meta_list = [{'name': t.name, 'description': t.description, 'inputSchema': t.inputSchema} for t in tools]
# Get datasets from SessionStore
store = SessionStore()
session_datasets = store.list_datasets() if hasattr(store, 'list_datasets') else []
session_ds_map = {ds['dataset_id']: ds for ds in session_datasets}
# Build dataset entries from ALL AnalysisRun records (upload/manual/auto, any status)
from analysis.models import AnalysisRun
all_runs = AnalysisRun.objects.all().order_by('-created_at')[:100]
datasets_with_status = []
for run in all_runs:
# Determine how the dataset was keyed in SessionStore
if run.run_type == 'upload':
ds_id = f'upload_{run.display_id}'
else:
ds_id = f'run_{run.display_id}'
session_entry = session_ds_map.get(ds_id)
if session_entry:
# Full data available in SessionStore — merge with run status
datasets_with_status.append({
**session_entry,
'dataset_id': ds_id,
'run_status': run.status,
'total_flows': run.total_flows,
'display_id': run.display_id,
'needs_reload': False,
})
elif run.csv_glob or run.sqlite_table:
# Data not in SessionStore but recoverable from disk/DB
datasets_with_status.append({
'dataset_id': ds_id,
'run_status': run.status,
'display_id': run.display_id,
'total_flows': run.total_flows,
'row_count': run.total_flows,
'file_count': None,
'column_count': 0,
'columns': [],
'svd_components': 0,
'needs_reload': True,
'csv_glob': run.csv_glob,
'sqlite_table': run.sqlite_table or '',
})
# Map statuses to user-facing labels for the filter tabs
# ready → 待分析, completed → 已分析, failed → 错误
# Everything else (pending/loading/profiling/aggregating/clustering/extracting) → analyzing → 分析中
for ds_info in datasets_with_status:
s = ds_info['run_status']
if s in ('completed',):
ds_info['status_label'] = '已分析'
ds_info['badge_class'] = 'completed'
elif s in ('failed',):
ds_info['status_label'] = '错误'
ds_info['badge_class'] = 'failed'
elif s in ('pending', 'loading', 'profiling', 'aggregating', 'clustering', 'extracting'):
ds_info['status_label'] = '分析中'
ds_info['badge_class'] = 'analyzing'
else:
ds_info['status_label'] = '待分析'
ds_info['badge_class'] = 'ready'
return render(request, 'tianxuan/manual.html', {
'core_tools': core_tools,
'diag_tools': diag_tools,
'analysis_tools': analysis_tools,
'other_tools': other_tools,
'tools_meta': _json.dumps(meta_list, ensure_ascii=False),
'datasets': datasets_with_status,
'datasets_with_status_json': _json.dumps(datasets_with_status, ensure_ascii=False),
})
@csrf_exempt
def manual_run_analysis(request):
"""AJAX POST: run clustering pipeline on the upload dataset."""
if request.method != 'POST':
return JsonResponse({'error': 'POST required'}, status=405)
import json as _json
body = _json.loads(request.body)
display_id = body.get('run_id')
run = get_object_or_404(AnalysisRun, display_id=display_id)
pk = run.id
run.run_type = 'manual'
run.save(update_fields=['run_type'])
feature_columns = body.get('feature_columns')
algorithm = body.get('algorithm', 'agglomerative')
min_cluster_size = int(body.get('min_cluster_size', 5))
head = body.get('head')
cluster_mode = body.get('cluster_mode', 'raw')
def _analysis_fn(run, ctx):
from analysis.session_store import SessionStore
feature_columns = ctx['feature_columns']
algorithm = ctx['algorithm']
min_cluster_size = ctx['min_cluster_size']
head = ctx.get('head')
pk = ctx['pk']
cluster_mode = ctx.get('cluster_mode', 'raw')
store = SessionStore()
try:
# Use display_id for dataset key (consistent with _background_process)
display_id = ctx.get('display_id', pk)
upload_ds_id = f'upload_{display_id}'
entry = store.get_dataset(upload_ds_id)
if entry is None:
# Fallback: try PK-based key (old pipeline)
entry = store.get_dataset(f'upload_{pk}')
if entry is None:
# Fallback: try entity dataset (from older pipeline)
entry = store.get_dataset(f'entity_{pk}')
if entry is None:
run.error_message = '请先上传并等待预处理完成'
run.status = 'failed'
run.save(update_fields=['status', 'error_message'])
return
ds_id = upload_ds_id if store.get_dataset(upload_ds_id) else f'upload_{pk}'
# Use the unified clustering pipeline (clustering → extraction → UMAP)
run_clustering_pipeline(
run=run, store=store, entity_ds_id=ds_id,
feature_columns=feature_columns,
algorithm=algorithm, min_cluster_size=min_cluster_size,
run_umap=True, head=head,
)
except Exception:
tb = traceback.format_exc()
logger.error(tb)
run.status = 'failed'
run.error_message = tb
run.progress_msg = f'失败: {tb}'
run.run_log += f'\n[ERROR] {tb}'
run.save(update_fields=['status', 'error_message', 'progress_msg', 'run_log'])
t = threading.Thread(
target=_run_pipeline_worker,
args=(pk, _analysis_fn),
kwargs=dict(feature_columns=feature_columns, algorithm=algorithm,
min_cluster_size=min_cluster_size, head=head, pk=pk,
display_id=run.display_id, cluster_mode=cluster_mode),
daemon=True,
)
t.start()
return JsonResponse({'status': 'started', 'redirect': f'/runs/{run.display_id}/'})