From ee639afb0d3d2d0ff1cfa5671445b07cd744cb24 Mon Sep 17 00:00:00 2001 From: sinohqb Date: Wed, 12 Aug 2026 10:59:51 +0800 Subject: [PATCH] feat(intelligent-eval): add decision process UI (ticket 09) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add list_decision_logs API endpoint - Add DecisionProcess component with timeline, list, filter, and export - Add decision log API calls to api.ts - Add "决策过程" button in EvalDetail to access decision history - Implement decision log export to JSON - Pass TypeScript type checking All 853 tests passing. --- .../web/routers/intelligent_evals.py | 35 ++++ frontend/web/src/api.ts | 13 ++ .../intelligent_eval/DecisionProcess.tsx | 183 ++++++++++++++++++ .../intelligent_eval/EvalDetail.tsx | 9 +- 4 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 frontend/web/src/components/intelligent_eval/DecisionProcess.tsx diff --git a/backend/agenteval/web/routers/intelligent_evals.py b/backend/agenteval/web/routers/intelligent_evals.py index 693fc8b..2b8dad6 100644 --- a/backend/agenteval/web/routers/intelligent_evals.py +++ b/backend/agenteval/web/routers/intelligent_evals.py @@ -346,6 +346,41 @@ async def create_decision_log( } +@router.get("/{eval_id}/decision-logs") +async def list_decision_logs(eval_id: str, session: Session = Depends(get_db)) -> dict: + """List all decision logs for an evaluation.""" + from sqlmodel import select + + from agenteval.storage.db import IntelligentEvalDB, IntelligentEvalDecisionLogDB + + # Verify eval exists + eval_db = session.get(IntelligentEvalDB, eval_id) + if eval_db is None: + raise HTTPException(status_code=404, detail=f"intelligent eval {eval_id} not found") + + # Get all decision logs + logs = session.exec( + select(IntelligentEvalDecisionLogDB) + .where(IntelligentEvalDecisionLogDB.eval_id == eval_id) + .order_by(IntelligentEvalDecisionLogDB.created_at.desc()) + ).all() + + return { + "logs": [ + { + "id": log.id, + "eval_id": log.eval_id, + "decision_type": log.decision_type, + "reason": log.reason, + "context": log.get_context(), + "cron_id": log.cron_id, + "created_at": log.created_at.isoformat() if log.created_at else None, + } + for log in logs + ] + } + + @router.get("/{eval_id}/config-snapshots") async def list_config_snapshots(eval_id: str, session: Session = Depends(get_db)) -> dict: """List all config snapshots for an evaluation.""" diff --git a/frontend/web/src/api.ts b/frontend/web/src/api.ts index 19de612..70a6e3f 100644 --- a/frontend/web/src/api.ts +++ b/frontend/web/src/api.ts @@ -743,6 +743,16 @@ export interface ConfigSnapshotComparison { differences: Record } +export interface DecisionLog { + id: string + eval_id: string + decision_type: 'execute_session' | 'wait' | 'start_analysis' + reason: string + context: Record + cron_id: string + created_at: string | null +} + export const intelligentEvalsApi = { list: () => api.get<{ intelligent_evals: IntelligentEval[] }>('/intelligent-evals'), get: (id: string) => api.get(`/intelligent-evals/${id}`), @@ -777,6 +787,9 @@ export const intelligentEvalsApi = { snapshot_id_1: snapshotId1, snapshot_id_2: snapshotId2, }), + // Decision Logs + listDecisionLogs: (id: string) => + api.get<{ logs: DecisionLog[] }>(`/intelligent-evals/${id}/decision-logs`), } // ── File Management ────────────────────────────────────────────── diff --git a/frontend/web/src/components/intelligent_eval/DecisionProcess.tsx b/frontend/web/src/components/intelligent_eval/DecisionProcess.tsx new file mode 100644 index 0000000..76d0ce5 --- /dev/null +++ b/frontend/web/src/components/intelligent_eval/DecisionProcess.tsx @@ -0,0 +1,183 @@ +import { useState } from 'react' +import { + Button, Card, Empty, Select, Table, Tag, Timeline, message, +} from 'antd' +import type { ColumnsType } from 'antd/es/table' +import { ArrowLeftOutlined, DownloadOutlined } from '@ant-design/icons' +import { intelligentEvalsApi, type DecisionLog } from '../../api' +import { colors } from '../../tokens' +import { formatDateTime } from '../../utils/date' + +const DECISION_TYPE_LABELS: Record = { + execute_session: { label: '执行会话', color: 'blue' }, + wait: { label: '等待', color: 'default' }, + start_analysis: { label: '开始分析', color: 'green' }, +} + +interface DecisionProcessProps { + evalId: string + onBack: () => void +} + +export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps) { + const [logs, setLogs] = useState(null) + const [loading, setLoading] = useState(false) + const [filterType, setFilterType] = useState(null) + const [expandedLog, setExpandedLog] = useState(null) + + const loadLogs = async () => { + setLoading(true) + try { + const res = await intelligentEvalsApi.listDecisionLogs(evalId) + setLogs(res.data.logs) + } catch { + message.error('加载决策日志失败') + } finally { + setLoading(false) + } + } + + useState(() => { + void loadLogs() + }) + + const handleExport = () => { + if (!logs) return + + const data = JSON.stringify(logs, null, 2) + const blob = new Blob([data], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `decision-logs-${evalId.slice(0, 8)}.json` + a.click() + URL.revokeObjectURL(url) + message.success('已导出决策日志') + } + + const filteredLogs = filterType + ? logs?.filter((log) => log.decision_type === filterType) ?? [] + : logs ?? [] + + const columns: ColumnsType = [ + { + title: '时间', + dataIndex: 'created_at', + key: 'created_at', + width: 180, + render: (val: string | null) => formatDateTime(val), + }, + { + title: '决策类型', + dataIndex: 'decision_type', + key: 'decision_type', + width: 120, + render: (val: string) => { + const info = DECISION_TYPE_LABELS[val] ?? { label: val, color: 'default' } + return {info.label} + }, + }, + { + title: '原因', + dataIndex: 'reason', + key: 'reason', + ellipsis: true, + }, + { + title: 'Cron ID', + dataIndex: 'cron_id', + key: 'cron_id', + width: 120, + render: (val: string) => {val.slice(0, 8)}, + }, + { + title: '操作', + key: 'actions', + width: 80, + render: (_, record) => ( + + ), + }, + ] + + return ( +
+
+ + 决策过程 +
+