feat: add quality evaluation center
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
ExperimentOutlined,
|
||||
FileSearchOutlined,
|
||||
LinkOutlined,
|
||||
PlayCircleOutlined,
|
||||
ReloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Alert, Button, Card, Empty, Progress, Select, Space, Statistic, Table, Tag, Typography, message } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { getPromptAssetDetail, listPromptAssets, runPromptAssetEvaluation } from "@/services/prompt-asset";
|
||||
import { useAuthStore } from "@/stores/auth.store";
|
||||
import type { PromptAssetDetailResponse, PromptAssetEvaluationRunItem, PromptAssetItem, PromptAssetType } from "@/types/api";
|
||||
|
||||
const { Paragraph, Text, Title } = Typography;
|
||||
|
||||
const promptTypeOptions: Array<{ value: PromptAssetType | "all"; label: string }> = [
|
||||
{ value: "all", label: "全部能力" },
|
||||
{ value: "contract_generation", label: "合同/文书生成" },
|
||||
{ value: "contract_review", label: "合同自动审查" },
|
||||
{ value: "clause_recommendation", label: "条款推荐" },
|
||||
{ value: "legal_chat", label: "法规问答" },
|
||||
{ value: "wording_rewrite", label: "措辞改写" },
|
||||
{ value: "regulation_impact", label: "法规影响分析" },
|
||||
];
|
||||
|
||||
const evaluationStatusOptions = [
|
||||
{ value: "all", label: "全部评测状态" },
|
||||
{ value: "passed", label: "评测通过" },
|
||||
{ value: "failed", label: "评测失败" },
|
||||
{ value: "pending", label: "等待评测" },
|
||||
{ value: "not_evaluated", label: "尚未评测" },
|
||||
{ value: "blocked", label: "发布受阻" },
|
||||
];
|
||||
|
||||
const statusOrder: Record<string, number> = {
|
||||
failed: 0,
|
||||
pending: 1,
|
||||
not_evaluated: 2,
|
||||
passed: 3,
|
||||
};
|
||||
|
||||
function formatDateTime(value?: string | null) {
|
||||
return value ? new Date(value).toLocaleString("zh-CN", { hour12: false }) : "-";
|
||||
}
|
||||
|
||||
function getPromptTypeLabel(value?: string | null) {
|
||||
return promptTypeOptions.find((item) => item.value === value)?.label ?? value ?? "-";
|
||||
}
|
||||
|
||||
function getEvaluationLabel(value?: string | null) {
|
||||
if (value === "passed") return "评测通过";
|
||||
if (value === "failed") return "评测失败";
|
||||
if (value === "pending") return "等待评测";
|
||||
return "尚未评测";
|
||||
}
|
||||
|
||||
function getEvaluationTag(value?: string | null) {
|
||||
if (value === "passed") return <Tag color="green">评测通过</Tag>;
|
||||
if (value === "failed") return <Tag color="red">评测失败</Tag>;
|
||||
if (value === "pending") return <Tag color="gold">等待评测</Tag>;
|
||||
return <Tag color="default">尚未评测</Tag>;
|
||||
}
|
||||
|
||||
function getPublishTag(item: PromptAssetItem) {
|
||||
if (item.status === "published") return <Tag color="green">已发布</Tag>;
|
||||
if (item.latest_evaluation_status === "passed") return <Tag color="blue">可发布</Tag>;
|
||||
return <Tag color="red">发布受阻</Tag>;
|
||||
}
|
||||
|
||||
function getEvaluationModeLabel(report?: Record<string, unknown> | null) {
|
||||
const mode = typeof report?.evaluation_mode === "string" ? report.evaluation_mode : "";
|
||||
if (mode === "llm") return "逐条大模型";
|
||||
if (mode === "local_fallback") return "本地兜底";
|
||||
if (mode === "llm_required_not_configured") return "模型未配置";
|
||||
if (mode === "local") return "本地规则";
|
||||
return "未记录";
|
||||
}
|
||||
|
||||
function getReportNumber(report: Record<string, unknown> | null | undefined, key: string) {
|
||||
const value = report?.[key];
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function getSampleCount(report?: Record<string, unknown> | null) {
|
||||
const sampleCount = getReportNumber(report, "sample_count");
|
||||
if (sampleCount != null) return sampleCount;
|
||||
const sampleResults = report?.sample_results;
|
||||
return Array.isArray(sampleResults) ? sampleResults.length : 0;
|
||||
}
|
||||
|
||||
function getPassedSamples(report?: Record<string, unknown> | null) {
|
||||
const passed = getReportNumber(report, "passed_samples");
|
||||
if (passed != null) return passed;
|
||||
const sampleResults = report?.sample_results;
|
||||
return Array.isArray(sampleResults) ? sampleResults.filter((item) => Boolean((item as { passed?: unknown }).passed)).length : 0;
|
||||
}
|
||||
|
||||
function getPassRateFromAsset(item: PromptAssetItem) {
|
||||
const sampleCount = getSampleCount(item.latest_evaluation_report);
|
||||
if (sampleCount > 0) {
|
||||
return Math.round((getPassedSamples(item.latest_evaluation_report) / sampleCount) * 100);
|
||||
}
|
||||
const score = item.latest_evaluation_score;
|
||||
if (typeof score === "number" && Number.isFinite(score)) {
|
||||
return Math.max(0, Math.min(100, Math.round(score)));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function sortByRisk(items: PromptAssetItem[]) {
|
||||
return [...items].sort((left, right) => {
|
||||
const leftOrder = statusOrder[left.latest_evaluation_status ?? "not_evaluated"] ?? 2;
|
||||
const rightOrder = statusOrder[right.latest_evaluation_status ?? "not_evaluated"] ?? 2;
|
||||
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
||||
return new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
export default function QualityEvalsPage() {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const currentUser = useAuthStore((state) => state.currentUser);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [runningId, setRunningId] = useState<string | null>(null);
|
||||
const [items, setItems] = useState<PromptAssetItem[]>([]);
|
||||
const [promptTypeFilter, setPromptTypeFilter] = useState<PromptAssetType | "all">("all");
|
||||
const [evaluationStatusFilter, setEvaluationStatusFilter] = useState("all");
|
||||
const [selectedDetail, setSelectedDetail] = useState<PromptAssetDetailResponse | null>(null);
|
||||
|
||||
const isAdmin = currentUser?.role === "admin";
|
||||
|
||||
const loadAssets = useCallback(async () => {
|
||||
if (!isAdmin) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const first = await listPromptAssets(1, 100, promptTypeFilter, "all");
|
||||
if (first.code !== 0) {
|
||||
messageApi.error(first.message || "质量评测数据加载失败");
|
||||
return;
|
||||
}
|
||||
const allItems = [...first.data.items];
|
||||
const totalPages = Math.ceil(first.data.total / first.data.page_size);
|
||||
for (let page = 2; page <= totalPages; page += 1) {
|
||||
const next = await listPromptAssets(page, 100, promptTypeFilter, "all");
|
||||
if (next.code === 0) {
|
||||
allItems.push(...next.data.items);
|
||||
}
|
||||
}
|
||||
setItems(sortByRisk(allItems));
|
||||
} catch (error) {
|
||||
messageApi.error(error instanceof Error ? error.message : "质量评测数据加载失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [isAdmin, messageApi, promptTypeFilter]);
|
||||
|
||||
const loadDetail = useCallback(
|
||||
async (assetId: string) => {
|
||||
try {
|
||||
const result = await getPromptAssetDetail(assetId);
|
||||
if (result.code !== 0) {
|
||||
messageApi.error(result.message || "评测历史加载失败");
|
||||
return null;
|
||||
}
|
||||
setSelectedDetail(result.data);
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
messageApi.error(error instanceof Error ? error.message : "评测历史加载失败");
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[messageApi],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void loadAssets();
|
||||
}, [loadAssets]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
if (evaluationStatusFilter === "all") return items;
|
||||
if (evaluationStatusFilter === "blocked") {
|
||||
return items.filter((item) => item.status !== "published" && item.latest_evaluation_status !== "passed");
|
||||
}
|
||||
return items.filter((item) => (item.latest_evaluation_status ?? "not_evaluated") === evaluationStatusFilter);
|
||||
}, [evaluationStatusFilter, items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedDetail && filteredItems[0]) {
|
||||
void loadDetail(filteredItems[0].id);
|
||||
}
|
||||
if (selectedDetail && !filteredItems.some((item) => item.id === selectedDetail.asset.id)) {
|
||||
setSelectedDetail(null);
|
||||
}
|
||||
}, [filteredItems, loadDetail, selectedDetail]);
|
||||
|
||||
const summary = useMemo(() => {
|
||||
const total = items.length;
|
||||
const passed = items.filter((item) => item.latest_evaluation_status === "passed").length;
|
||||
const failed = items.filter((item) => item.latest_evaluation_status === "failed").length;
|
||||
const pending = items.filter((item) => item.latest_evaluation_status === "pending").length;
|
||||
const notEvaluated = items.filter((item) => !item.latest_evaluation_status || item.latest_evaluation_status === "not_evaluated").length;
|
||||
const publishBlocked = items.filter((item) => item.status !== "published" && item.latest_evaluation_status !== "passed").length;
|
||||
const totalSamples = items.reduce((sum, item) => sum + getSampleCount(item.latest_evaluation_report), 0);
|
||||
const passedSamples = items.reduce((sum, item) => sum + getPassedSamples(item.latest_evaluation_report), 0);
|
||||
return {
|
||||
total,
|
||||
passed,
|
||||
failed,
|
||||
pending,
|
||||
notEvaluated,
|
||||
publishBlocked,
|
||||
samplePassRate: totalSamples > 0 ? Math.round((passedSamples / totalSamples) * 100) : 0,
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
const handleRunEvaluation = async (item: PromptAssetItem) => {
|
||||
setRunningId(item.id);
|
||||
try {
|
||||
const result = await runPromptAssetEvaluation(item.id);
|
||||
if (result.code !== 0) {
|
||||
messageApi.error(result.message || "运行评测失败");
|
||||
return;
|
||||
}
|
||||
messageApi.success(`评测完成:${getEvaluationLabel(result.data.version.evaluation_status)}`);
|
||||
await loadAssets();
|
||||
await loadDetail(item.id);
|
||||
} catch (error) {
|
||||
messageApi.error(error instanceof Error ? error.message : "运行评测失败");
|
||||
} finally {
|
||||
setRunningId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<PromptAssetItem> = [
|
||||
{
|
||||
title: "评测对象",
|
||||
key: "asset",
|
||||
width: 260,
|
||||
render: (_, item) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text strong>{item.name}</Text>
|
||||
<Text code>{item.prompt_key}</Text>
|
||||
<Text type="secondary">{getPromptTypeLabel(item.prompt_type)}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "质量门禁",
|
||||
key: "gate",
|
||||
width: 160,
|
||||
render: (_, item) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
{getEvaluationTag(item.latest_evaluation_status)}
|
||||
{getPublishTag(item)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "样本通过率",
|
||||
key: "passRate",
|
||||
width: 180,
|
||||
render: (_, item) => (
|
||||
<Space direction="vertical" size={0} style={{ width: "100%" }}>
|
||||
<Progress percent={getPassRateFromAsset(item)} size="small" status={item.latest_evaluation_status === "failed" ? "exception" : "normal"} />
|
||||
<Text type="secondary">
|
||||
{getPassedSamples(item.latest_evaluation_report)}/{getSampleCount(item.latest_evaluation_report)} 条样本
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "执行方式",
|
||||
key: "mode",
|
||||
width: 130,
|
||||
render: (_, item) => <Tag color="blue">{getEvaluationModeLabel(item.latest_evaluation_report)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "当前版本",
|
||||
key: "version",
|
||||
width: 130,
|
||||
render: (_, item) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text>{item.current_version_label}</Text>
|
||||
<Text type="secondary">{item.version_count} 个版本</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "更新时间",
|
||||
dataIndex: "updated_at",
|
||||
key: "updated_at",
|
||||
width: 180,
|
||||
render: (value: string) => formatDateTime(value),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: 260,
|
||||
render: (_, item) => (
|
||||
<Space wrap>
|
||||
<Button icon={<FileSearchOutlined />} size="small" onClick={() => void loadDetail(item.id)}>
|
||||
历史
|
||||
</Button>
|
||||
<Button icon={<PlayCircleOutlined />} loading={runningId === item.id} size="small" type="primary" onClick={() => void handleRunEvaluation(item)}>
|
||||
运行评测
|
||||
</Button>
|
||||
<Button icon={<LinkOutlined />} size="small">
|
||||
<Link href="/prompt-assets">治理</Link>
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const evaluationRunColumns: ColumnsType<PromptAssetEvaluationRunItem> = [
|
||||
{
|
||||
title: "套件",
|
||||
dataIndex: "suite_key",
|
||||
key: "suite_key",
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: "状态",
|
||||
key: "status",
|
||||
width: 120,
|
||||
render: (_, item) => getEvaluationTag(item.status),
|
||||
},
|
||||
{
|
||||
title: "样本",
|
||||
key: "samples",
|
||||
width: 120,
|
||||
render: (_, item) => `${item.passed_samples}/${item.sample_count}`,
|
||||
},
|
||||
{
|
||||
title: "通过率",
|
||||
key: "pass_rate",
|
||||
width: 140,
|
||||
render: (_, item) => `${Math.round((item.pass_rate ?? 0) * 100)}%`,
|
||||
},
|
||||
{
|
||||
title: "分数",
|
||||
key: "quality_score",
|
||||
width: 100,
|
||||
render: (_, item) => item.quality_score ?? "-",
|
||||
},
|
||||
{
|
||||
title: "执行方式",
|
||||
key: "mode",
|
||||
width: 150,
|
||||
render: (_, item) => <Tag color="blue">{getEvaluationModeLabel(item.report)}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "失败原因",
|
||||
dataIndex: "failure_reason",
|
||||
key: "failure_reason",
|
||||
ellipsis: true,
|
||||
render: (value: string | null) => value ?? "-",
|
||||
},
|
||||
{
|
||||
title: "完成时间",
|
||||
dataIndex: "finished_at",
|
||||
key: "finished_at",
|
||||
width: 180,
|
||||
render: (value: string | null) => formatDateTime(value),
|
||||
},
|
||||
];
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<main className="page-shell review-history-page">
|
||||
{contextHolder}
|
||||
<Card className="review-list-card">
|
||||
<Alert title="只有管理员可以查看质量评测中心。" type="warning" showIcon />
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="page-shell review-page-grid">
|
||||
{contextHolder}
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<section className="review-page-hero">
|
||||
<div className="review-page-hero-copy">
|
||||
<span className="landing-badge">质量评测中心</span>
|
||||
<Title level={2}>黄金样本评测与发布门禁</Title>
|
||||
<Text type="secondary">
|
||||
汇总生成、审查、法规问答、条款推荐、措辞改写和法规影响分析的 Prompt 评测结果,把原来分散在 Prompt 治理里的质量状态变成独立入口。
|
||||
</Text>
|
||||
</div>
|
||||
<Card className="review-side-card" variant="borderless">
|
||||
<span className="landing-panel-eyebrow">Phase15-F</span>
|
||||
<Title level={4} style={{ margin: 0, color: "inherit" }}>
|
||||
发布前质量门禁
|
||||
</Title>
|
||||
<Paragraph style={{ color: "inherit", opacity: 0.82, marginBottom: 0 }}>
|
||||
未通过评测的 Prompt 不能发布;已发布 Prompt 可在治理页继续回滚、下线和查看运行证据。
|
||||
</Paragraph>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 12 }}>
|
||||
<Card>
|
||||
<Statistic title="评测对象" value={summary.total} prefix={<ExperimentOutlined />} />
|
||||
</Card>
|
||||
<Card>
|
||||
<Statistic title="通过门禁" value={summary.passed} prefix={<CheckCircleOutlined />} valueStyle={{ color: "#237804" }} />
|
||||
</Card>
|
||||
<Card>
|
||||
<Statistic title="失败或待处理" value={summary.failed + summary.pending + summary.notEvaluated} prefix={<CloseCircleOutlined />} valueStyle={{ color: "#a8071a" }} />
|
||||
</Card>
|
||||
<Card>
|
||||
<Statistic title="发布受阻" value={summary.publishBlocked} prefix={<ClockCircleOutlined />} valueStyle={{ color: "#ad6800" }} />
|
||||
</Card>
|
||||
<Card>
|
||||
<Statistic title="样本通过率" value={summary.samplePassRate} suffix="%" prefix={<CheckCircleOutlined />} />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
||||
<Space wrap>
|
||||
<Select style={{ width: 190 }} options={promptTypeOptions} value={promptTypeFilter} onChange={(value) => setPromptTypeFilter(value)} />
|
||||
<Select style={{ width: 170 }} options={evaluationStatusOptions} value={evaluationStatusFilter} onChange={setEvaluationStatusFilter} />
|
||||
<Button icon={<ReloadOutlined />} loading={loading} onClick={() => void loadAssets()}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button icon={<LinkOutlined />}>
|
||||
<Link href="/prompt-assets">进入 Prompt 治理</Link>
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredItems}
|
||||
loading={loading}
|
||||
locale={{ emptyText: <Empty description="暂无质量评测对象" /> }}
|
||||
pagination={{ pageSize: 10, showSizeChanger: true }}
|
||||
rowKey="id"
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title={selectedDetail ? `${selectedDetail.asset.name} 评测历史` : "评测历史"}>
|
||||
{selectedDetail ? (
|
||||
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
||||
<Alert
|
||||
message="当前版本质量状态"
|
||||
description={`${selectedDetail.asset.current_version_label}:${getEvaluationLabel(selectedDetail.asset.latest_evaluation_status)},执行方式:${getEvaluationModeLabel(selectedDetail.asset.latest_evaluation_report)}。`}
|
||||
type={selectedDetail.asset.latest_evaluation_status === "passed" ? "success" : "warning"}
|
||||
showIcon
|
||||
/>
|
||||
<Table columns={evaluationRunColumns} dataSource={selectedDetail.evaluation_runs} pagination={false} rowKey="id" size="small" />
|
||||
</Space>
|
||||
) : (
|
||||
<Empty description="请选择一个评测对象查看历史" />
|
||||
)}
|
||||
</Card>
|
||||
</Space>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DashboardOutlined,
|
||||
DeploymentUnitOutlined,
|
||||
DownOutlined,
|
||||
ExperimentOutlined,
|
||||
FileDoneOutlined,
|
||||
FileTextOutlined,
|
||||
FileSearchOutlined,
|
||||
@@ -87,6 +88,7 @@ const adminNavItems = [
|
||||
{ href: "/templates", label: "模板管理", icon: <BookOutlined /> },
|
||||
{ href: "/knowledge-bases", label: "知识库管理", icon: <FolderOpenOutlined /> },
|
||||
{ href: "/prompt-assets", label: "Prompt 治理", icon: <CodeOutlined /> },
|
||||
{ href: "/quality-evals", label: "质量评测", icon: <ExperimentOutlined /> },
|
||||
{ href: "/legal-change-impacts", label: "法规影响分析", icon: <AlertOutlined /> },
|
||||
{ href: "/operation-kpis", label: "运营 KPI", icon: <BarChartOutlined /> },
|
||||
{ href: "/sales-followups", label: "销售跟进", icon: <PhoneOutlined /> },
|
||||
|
||||
@@ -4211,7 +4211,7 @@
|
||||
- 本轮核对结论:
|
||||
- Phase15-A 到 Phase15-H 的主线开发已经在代码层闭环。
|
||||
- `/clauses/apply` 未单独实现,但条款推荐和改写结果已经通过 `/documents/[id]/edit` 编辑器写入合同正文并生成 `generated_contract_versions`,属于等价闭环。
|
||||
- `/quality-evals` 未单独实现,当前评测入口集中在 `/prompt-assets`,已能支撑 Prompt 发布前评测门禁;如要做统一质量中心,后续可单独补页面和通用评测表。
|
||||
- `/quality-evals` 在本轮核对当时尚未单独实现,评测入口集中在 `/prompt-assets`;该缺口已在后续“Phase15-F 独立质量评测中心 `/quality-evals` 本地闭环”中补齐。
|
||||
- 法规影响分析代码统一使用 `/legal-change-impacts`、`legal_change_events`、`legal_change_impacts` 命名,后续文档应以此为准。
|
||||
- 本轮验证:
|
||||
- `$env:PYTHONPATH='backend'; python -m unittest backend.tests.test_legal_document_type_service backend.tests.test_legal_document_types_api backend.tests.test_legal_document_element_schema_service backend.tests.test_legal_document_element_schemas_api backend.tests.test_legal_document_draft_service backend.tests.test_legal_document_drafts_api backend.tests.test_contracts_api backend.tests.test_review_risk_action_service backend.tests.test_review_risk_actions_api backend.tests.test_clause_recommendation_service backend.tests.test_clauses_api backend.tests.test_prompt_asset_service backend.tests.test_prompt_assets_api backend.tests.test_prompt_runtime_service backend.tests.test_legal_change_impact_service backend.tests.test_legal_change_impacts_api backend.tests.test_operation_kpi_service backend.tests.test_operation_kpis_api`
|
||||
@@ -4223,7 +4223,7 @@
|
||||
- 剩余边界:
|
||||
- 当前仍有 Phase15-C、Phase15-F、Phase15-G 相关未提交改动,需要后续统一整理提交。
|
||||
- 最新代码尚未部署到 `192.168.3.90` 做真实库烟测。
|
||||
- 专业富文本库、字段级 diff、独立 `/quality-evals`、全量法规向量化、真实微信或支付宝支付、电子签章和小程序均属于后续增强,不是当前法律文书能力吸收主线阻塞。
|
||||
- 专业富文本库、字段级 diff、全量法规向量化、真实微信或支付宝支付、电子签章和小程序均属于后续增强,不是当前法律文书能力吸收主线阻塞;独立 `/quality-evals` 已在后续闭环中补齐。
|
||||
|
||||
## 2026-05-21 Phase15-C/F/G 提交推送与 3.90 真实库烟测闭环
|
||||
|
||||
@@ -4270,6 +4270,32 @@
|
||||
- 远程真实 PostgreSQL 已在 `20260521_0038 (head)`。
|
||||
- 法规影响 worker 已在测试服务器常驻运行,并通过真实库临时事件消费烟测。
|
||||
- 剩余边界:
|
||||
- 本轮没有做 `/quality-evals` 独立质量中心。
|
||||
- 本轮当时没有做 `/quality-evals` 独立质量中心;该项已在后续“Phase15-F 独立质量评测中心 `/quality-evals` 本地闭环”中补齐。
|
||||
- 本轮没有做全量法规向量化。
|
||||
- 本轮没有接入真实微信或支付宝支付。
|
||||
|
||||
## 2026-05-21 Phase15-F 独立质量评测中心 `/quality-evals` 本地闭环
|
||||
|
||||
- 已按《AI合同起草与审查助手-法律文书能力吸收开发方案-2026-05-20.md》补齐文档提到的独立质量评测产品入口。
|
||||
- 本轮新增前端页面:
|
||||
- `frontend/src/app/quality-evals/page.tsx`
|
||||
- 管理员菜单新增“质量评测”,入口位于 `frontend/src/components/layout/app-topbar.tsx`。
|
||||
- 当前实现口径:
|
||||
- 不新增重复的质量评测后端模型,继续以 `prompt_assets`、`prompt_asset_versions`、`prompt_asset_evaluation_runs` 作为黄金样本评测真相源。
|
||||
- `/quality-evals` 负责聚合生成、审查、法规问答、条款推荐、措辞改写、法规影响分析等 Prompt 类型的评测状态。
|
||||
- 页面支持按能力类型、评测状态、发布受阻筛选。
|
||||
- 页面展示评测对象、质量门禁、样本通过率、执行方式、当前版本、更新时间和评测历史。
|
||||
- 页面可直接触发 Prompt 黄金样本评测,并可跳转 `/prompt-assets` 做发布、下线、回滚和运行证据治理。
|
||||
- 已更新代码对齐检查报告:
|
||||
- `project-docs/AI合同起草与审查助手-法律文书能力吸收开发方案-代码对齐检查-2026-05-21.md`
|
||||
- 旧结论“没有独立 `/quality-evals` 页面”已改为“独立入口已闭环”。
|
||||
- 本地验证:
|
||||
- `cd frontend; npx eslint "src/app/quality-evals/page.tsx" "src/components/layout/app-topbar.tsx"`
|
||||
- `cd frontend; npm run build`
|
||||
- 构建结果:`Compiled successfully`,静态页面生成 `40/40`,路由清单包含 `/quality-evals`。
|
||||
- 当前闭环结论:
|
||||
- 文档提到的“独立 `/quality-evals` 页面”已经完成本地开发闭环。
|
||||
- Phase15-F 现在形成“Prompt 治理评测真相源 -> `/quality-evals` 独立质量入口 -> 运行评测 -> 查看历史 -> 发布门禁 -> 回到 Prompt 治理”的产品入口闭环。
|
||||
- 剩余边界:
|
||||
- 本轮暂未新增通用 `ai_quality_eval_runs/items` 表;该表只在未来要把非 Prompt 质量评测也做成独立后端领域时再补。
|
||||
- 下一步需要提交推送,并部署到 `192.168.3.90` 后访问 `/quality-evals` 做前端路由烟测。
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
|
||||
当前代码已经覆盖 Phase15-A 到 Phase15-H 的主线开发闭环。也就是说,文书类型入口、动态要素表单、草稿保存恢复、生成接口升级、审查建议采纳、条款推荐与行业惯例、富文本编辑器和 AI 侧边栏、Prompt 治理、黄金样本评测、法规影响扫描、运营 KPI 与商业化增强,均已在代码层形成可运行闭环。
|
||||
|
||||
但仍有三类事项需要分开看:
|
||||
截至本轮独立质量评测中心收口后,仍有三类事项需要分开看:
|
||||
|
||||
1. 已开发但待提交或待部署验证:当前工作区存在 Phase15-C、Phase15-F、Phase15-G 的未提交改动,且最新 worker 和语义扫描未在 `192.168.3.90` 做远程部署烟测。
|
||||
2. 文档建议路径与代码等价实现不同:例如文档建议 `/quality-evals` 和 `/clauses/apply`,当前分别落在 `/prompt-assets` 评测历史和合同编辑器版本链中。
|
||||
3. 后续增强项:专业富文本库、字段级差异对比、独立质量评测中心、全量法规向量化、真实微信或支付宝支付、电子签章、小程序等不属于当前 Phase15 主线硬阻塞。
|
||||
1. 本轮新增 `/quality-evals` 独立页面,不新增数据库迁移,继续复用 Prompt 评测运行记录作为质量评测真相源。
|
||||
2. 文档建议路径与代码仍保留一个等价实现差异:文档建议 `/clauses/apply`,当前落在合同编辑器版本链中;`/quality-evals` 已补成独立产品入口。
|
||||
3. 后续增强项:专业富文本库、字段级差异对比、通用 `ai_quality_eval_runs/items`、全量法规向量化、真实微信或支付宝支付、电子签章、小程序等不属于当前 Phase15 主线硬阻塞。
|
||||
|
||||
## 三、逐阶段对齐表
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
| LAW-CLAUSE-002 | 措辞优化 | 支持专业化、简化、偏甲方、偏乙方、均衡等改写模式,返回法律边界和谈判建议 | 已闭环 |
|
||||
| Phase15-D | 富文本编辑器与 AI 侧边栏 | `/documents/[id]/edit` 支持正文编辑、自动保存、版本历史、条款推荐、措辞改写、相关法条和 Chat 入口 | 主线已闭环 |
|
||||
| Phase15-E | Prompt 资产治理 | `/prompt-assets` 支持 Prompt 类型、版本、评测、发布、下线、回滚和运行证据 | 已闭环 |
|
||||
| Phase15-F | 黄金样本与质量评测 | 已有 Prompt 评测运行记录、逐条黄金样本报告、发布前门禁和前端评测历史 | 已闭环 |
|
||||
| Phase15-F | 黄金样本与质量评测 | 已有 Prompt 评测运行记录、逐条黄金样本报告、发布前门禁、`/prompt-assets` 治理入口和 `/quality-evals` 独立质量评测中心 | 已闭环 |
|
||||
| Phase15-G | 法规变更影响分析 | 已有法条变更入队、结构化和语义影响扫描、影响项、页面、后台 worker 和 Docker 服务 | 已闭环 |
|
||||
| Phase15-H | 运营 KPI 与商业化增强 | `/operation-kpis` 已展示用量、文书生成、审查、Chat、模板、条款采纳、试用和转化指标;计费、发票状态、销售跟进已有业务闭环 | 主线已闭环 |
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
- 合同编辑器和 AI 侧边栏:`frontend/src/app/documents/[id]/edit/page.tsx`
|
||||
- 审查建议处理:`frontend/src/app/reviews/[id]/page.tsx`
|
||||
- Prompt 治理:`frontend/src/app/prompt-assets/page.tsx`
|
||||
- 质量评测中心:`frontend/src/app/quality-evals/page.tsx`
|
||||
- 法规影响分析:`frontend/src/app/legal-change-impacts/page.tsx`
|
||||
- 运营 KPI:`frontend/src/app/operation-kpis/page.tsx`
|
||||
- 销售跟进:`frontend/src/app/sales-followups/page.tsx`
|
||||
@@ -108,14 +109,15 @@
|
||||
|
||||
### 5.2 `/quality-evals`
|
||||
|
||||
文档建议新增独立 `/quality-evals` 页面。当前代码没有这个独立路由,质量评测落在 `/prompt-assets` 中:
|
||||
文档建议新增独立 `/quality-evals` 页面。本轮已经补齐独立路由,并保持评测数据真相源不重复:
|
||||
|
||||
- Prompt 详情页可以运行评测。
|
||||
- 评测结果回填当前版本。
|
||||
- 发布按钮受评测状态约束。
|
||||
- 评测运行历史可以追溯。
|
||||
- `/quality-evals` 聚合生成、审查、法规问答、条款推荐、措辞改写、法规影响分析等 Prompt 类型的评测状态。
|
||||
- 页面支持按 Prompt 类型、评测状态和“发布受阻”筛选。
|
||||
- 页面展示评测对象、质量门禁、样本通过率、执行方式、版本、更新时间和评测历史。
|
||||
- 页面可直接触发 `POST /api/v1/prompt-assets/{asset_id}/evaluations/run` 重新运行黄金样本评测。
|
||||
- 页面保留跳转 `/prompt-assets` 的治理入口,发布、下线、回滚和运行证据仍由 Prompt 治理承接。
|
||||
|
||||
这对“Prompt 发布前黄金样本评测门禁”已经闭环。但如果要做跨生成、审查、Chat、条款推荐、法规影响的统一质量中心,仍建议后续补独立 `/quality-evals`。
|
||||
因此,“文档要求独立质量评测产品入口”已经闭环;后续只有在需要把非 Prompt 质量评测也沉淀为独立领域模型时,才需要另补通用 `ai_quality_eval_runs/items` 表。
|
||||
|
||||
### 5.3 `regulation-changes`
|
||||
|
||||
@@ -131,19 +133,20 @@
|
||||
|
||||
### 6.1 待提交和待部署验证
|
||||
|
||||
当前工作区存在未提交改动,主要集中在:
|
||||
Phase15-C、Phase15-F、Phase15-G 已完成提交、推送和 `192.168.3.90` 真实库烟测,详见当前工作进度保存文档。
|
||||
|
||||
- Phase15-C 行业惯例推荐。
|
||||
- Phase15-F 真实黄金样本逐条评测。
|
||||
- Phase15-G 法规影响语义扫描和后台 worker。
|
||||
|
||||
这些代码本地测试已通过,但尚未提交远程 Git,也未在 `192.168.3.90` 重新部署后做真实库烟测。
|
||||
本轮新增 `/quality-evals` 不涉及 Alembic 迁移或后端服务变更。完成提交后,只需要部署前端并访问 `/quality-evals` 做页面烟测。
|
||||
|
||||
### 6.2 独立质量评测中心
|
||||
|
||||
当前评测能力已经在 Prompt 治理内闭环,但没有独立 `/quality-evals` 页面,也没有通用 `ai_quality_eval_runs/items` 表承接所有质量评测类型。
|
||||
独立 `/quality-evals` 页面已经补齐,当前闭环采用“独立页面 + 复用 Prompt 评测真相源”的方式:
|
||||
|
||||
这不是 Prompt 发布门禁阻塞项,但属于文档和产品形态的一个剩余增强点。
|
||||
- 前端入口:`frontend/src/app/quality-evals/page.tsx`。
|
||||
- 菜单入口:管理员菜单“质量评测”。
|
||||
- 数据来源:`prompt_assets`、`prompt_asset_versions`、`prompt_asset_evaluation_runs`。
|
||||
- 执行动作:复用 Prompt 资产评测接口重新运行黄金样本评测。
|
||||
|
||||
当前仍未单独新增通用 `ai_quality_eval_runs/items` 表。这个表属于后续“跨 Prompt、跨业务对象的统一质量评测域”增强,不影响 Phase15-F 的产品入口闭环。
|
||||
|
||||
### 6.3 专业级富文本增强
|
||||
|
||||
@@ -194,7 +197,7 @@ OK
|
||||
命令:
|
||||
|
||||
```powershell
|
||||
npx eslint "src/app/documents/new/page.tsx" "src/app/documents/new/[type]/page.tsx" "src/app/documents/[id]/edit/page.tsx" "src/app/prompt-assets/page.tsx" "src/app/legal-change-impacts/page.tsx" "src/app/operation-kpis/page.tsx" "src/components/layout/app-topbar.tsx" "src/types/api.ts"
|
||||
npx eslint "src/app/documents/new/page.tsx" "src/app/documents/new/[type]/page.tsx" "src/app/documents/[id]/edit/page.tsx" "src/app/prompt-assets/page.tsx" "src/app/quality-evals/page.tsx" "src/app/legal-change-impacts/page.tsx" "src/app/operation-kpis/page.tsx" "src/components/layout/app-topbar.tsx" "src/types/api.ts"
|
||||
```
|
||||
|
||||
结果:通过,无错误输出。
|
||||
@@ -211,7 +214,7 @@ npm run build
|
||||
|
||||
```text
|
||||
Compiled successfully
|
||||
Generating static pages (39/39)
|
||||
Generating static pages (40/40)
|
||||
```
|
||||
|
||||
构建路由中已包含:
|
||||
@@ -220,6 +223,7 @@ Generating static pages (39/39)
|
||||
- `/documents/new/[type]`
|
||||
- `/documents/[id]/edit`
|
||||
- `/prompt-assets`
|
||||
- `/quality-evals`
|
||||
- `/legal-change-impacts`
|
||||
- `/operation-kpis`
|
||||
- `/sales-followups`
|
||||
@@ -228,9 +232,9 @@ Generating static pages (39/39)
|
||||
|
||||
为了让文档和代码彻底闭合,建议下一步不要继续堆新功能,先按这个顺序收口:
|
||||
|
||||
1. 整理并提交当前 Phase15-C、Phase15-F、Phase15-G 的代码和文档改动。
|
||||
2. 如果本阶段要求“文档路径和产品入口完全一致”,补独立 `/quality-evals` 质量评测中心,把 `/prompt-assets` 中的评测历史聚合过去。
|
||||
1. 提交并推送本轮 `/quality-evals` 页面和文档更新。
|
||||
2. 部署前端到 `192.168.3.90`,访问 `/quality-evals` 做页面烟测。
|
||||
3. 统一文档命名,把法规影响分析的接口名称固定为 `/legal-change-impacts`,避免后续误认为还缺 `/regulation-changes`。
|
||||
4. 等决定部署时,在 `192.168.3.90` 执行最新 Alembic,并启用 `legal-change-impact-worker` 服务做真实库烟测。
|
||||
4. 如未来要做更宽的统一质量平台,再补通用 `ai_quality_eval_runs/items` 和非 Prompt 质量评测对象。
|
||||
|
||||
按当前代码状态判断,《法律文书能力吸收开发方案》的主线开发没有跑偏,已经基本完成。真正剩下的是提交、部署验证、独立质量中心和若干生产级增强。
|
||||
按当前代码状态判断,《法律文书能力吸收开发方案》的主线开发没有跑偏,已经基本完成。`/quality-evals` 独立入口补齐后,真正剩下的是若干生产级增强,而不是 Phase15 主线功能缺口。
|
||||
|
||||
Reference in New Issue
Block a user