feat: enhance ai quality improvement operations
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# V2 AI 质量改进项运营增强记录 - 2026-06-23
|
||||
|
||||
## 本轮目标
|
||||
|
||||
延续 AI 质量改进动作闭环,补齐管理员日常运营需要的筛选和效果复盘能力,避免改进项只停留在“创建/关闭”,而是能持续看出处理后是否改善。
|
||||
|
||||
## 已完成能力
|
||||
|
||||
1. AI Platform 改进项列表接口新增过滤参数:
|
||||
- `status`
|
||||
- `owner`
|
||||
- `priority`
|
||||
- `action_type`
|
||||
- `dimension`
|
||||
2. 改进项响应新增效果复盘字段:
|
||||
- `effect_status`: `pending_data`、`improved`、`unchanged`、`regressed`
|
||||
- `effect_summary`: 基线异常率、当前异常率、待处理标签变化、质量分变化
|
||||
3. 内存仓库与 SQL 仓库统一支持筛选和效果复盘。
|
||||
4. OpenAPI 契约同步新增查询参数和效果字段。
|
||||
5. Python SDK 同步支持筛选参数和效果字段。
|
||||
6. 合同系统 AI 质量运营页新增改进项筛选栏:
|
||||
- 状态
|
||||
- 负责人
|
||||
- 优先级
|
||||
- 动作类型
|
||||
- 归因维度
|
||||
7. 改进项卡片新增效果标签和复盘摘要,管理员可以直接看到处理后异常率是否下降。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```powershell
|
||||
python -m pytest yuqei-ai-platform/services/ai-platform-api/tests yuqei-ai-sdk-python/tests -q
|
||||
python -c "import yaml; yaml.safe_load(open('yuqei-ai-platform/packages/ai-openapi/openapi.yaml', encoding='utf-8')); print('openapi ok')"
|
||||
npm run web:typecheck
|
||||
npm run web:build
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## 下一步建议
|
||||
|
||||
继续围绕 V2 方案推进 AI 中台主线,下一步可进入 Prompt 版本发布/回滚闭环,或知识库导入批次与改进项关联,让“调整 Prompt”“补知识库”不只是文字状态,而能关联到真实配置变更记录。
|
||||
@@ -480,6 +480,31 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
enum: [open, in_progress, resolved, false_positive, closed]
|
||||
- name: owner
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 128
|
||||
- name: priority
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [low, medium, high, critical]
|
||||
- name: action_type
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [investigate, tune_prompt, improve_knowledge, tune_model, mark_false_positive]
|
||||
- name: dimension
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [model, prompt_template, knowledge_base, label_code]
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
@@ -1636,6 +1661,12 @@ components:
|
||||
quality_score_delta:
|
||||
type: number
|
||||
format: float
|
||||
effect_status:
|
||||
type: string
|
||||
enum: [pending_data, improved, unchanged, regressed]
|
||||
effect_summary:
|
||||
type: string
|
||||
nullable: true
|
||||
resolution_note:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
@@ -256,9 +256,23 @@ def create_app(
|
||||
)
|
||||
def list_quality_improvement_actions(
|
||||
status: str | None = Query(None, pattern="^(open|in_progress|resolved|false_positive|closed)$"),
|
||||
owner: str | None = Query(None, min_length=1, max_length=128),
|
||||
priority: str | None = Query(None, pattern="^(low|medium|high|critical)$"),
|
||||
action_type: str | None = Query(
|
||||
None,
|
||||
pattern="^(investigate|tune_prompt|improve_knowledge|tune_model|mark_false_positive)$",
|
||||
),
|
||||
dimension: str | None = Query(None, pattern="^(model|prompt_template|knowledge_base|label_code)$"),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
) -> list[AiQualityImprovementAction]:
|
||||
return store.list_quality_improvement_actions(status=status, limit=limit)
|
||||
return store.list_quality_improvement_actions(
|
||||
status=status,
|
||||
owner=owner,
|
||||
priority=priority,
|
||||
action_type=action_type,
|
||||
dimension=dimension,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
@app.post(
|
||||
f"{resolved_settings.api_prefix}/audit/quality-improvement-actions",
|
||||
|
||||
+101
-7
@@ -348,6 +348,8 @@ class AiQualityImprovementAction(AiQualityImprovementActionCreate):
|
||||
affected_rate_delta: float = 0.0
|
||||
pending_labels_delta: int = 0
|
||||
quality_score_delta: float = 0.0
|
||||
effect_status: str = "pending_data"
|
||||
effect_summary: str | None = None
|
||||
resolution_note: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
@@ -448,6 +450,10 @@ class AiPlatformRepository(Protocol):
|
||||
self,
|
||||
*,
|
||||
status: str | None = None,
|
||||
owner: str | None = None,
|
||||
priority: str | None = None,
|
||||
action_type: str | None = None,
|
||||
dimension: str | None = None,
|
||||
limit: int = 20,
|
||||
) -> list[AiQualityImprovementAction]: ...
|
||||
|
||||
@@ -760,6 +766,10 @@ class InMemoryAiPlatformRepository:
|
||||
self,
|
||||
*,
|
||||
status: str | None = None,
|
||||
owner: str | None = None,
|
||||
priority: str | None = None,
|
||||
action_type: str | None = None,
|
||||
dimension: str | None = None,
|
||||
limit: int = 20,
|
||||
) -> list[AiQualityImprovementAction]:
|
||||
with self._lock:
|
||||
@@ -767,7 +777,15 @@ class InMemoryAiPlatformRepository:
|
||||
self.quality_improvement_actions.values(),
|
||||
self.ai_run_audits,
|
||||
)
|
||||
return _filter_improvement_actions(actions, status=status, limit=limit)
|
||||
return _filter_improvement_actions(
|
||||
actions,
|
||||
status=status,
|
||||
owner=owner,
|
||||
priority=priority,
|
||||
action_type=action_type,
|
||||
dimension=dimension,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
def create_quality_improvement_action(
|
||||
self,
|
||||
@@ -793,7 +811,7 @@ class InMemoryAiPlatformRepository:
|
||||
**payload.model_dump(exclude={"title"}),
|
||||
)
|
||||
self.quality_improvement_actions[action.id] = action
|
||||
return action
|
||||
return _materialize_improvement_action(action, self.ai_run_audits)
|
||||
|
||||
def update_quality_improvement_action(
|
||||
self,
|
||||
@@ -1354,6 +1372,10 @@ class SqlAlchemyAiPlatformRepository:
|
||||
self,
|
||||
*,
|
||||
status: str | None = None,
|
||||
owner: str | None = None,
|
||||
priority: str | None = None,
|
||||
action_type: str | None = None,
|
||||
dimension: str | None = None,
|
||||
limit: int = 20,
|
||||
) -> list[AiQualityImprovementAction]:
|
||||
with self._session_factory() as session:
|
||||
@@ -1362,6 +1384,14 @@ class SqlAlchemyAiPlatformRepository:
|
||||
)
|
||||
if status:
|
||||
statement = statement.where(AiQualityImprovementActionModel.status == status)
|
||||
if owner:
|
||||
statement = statement.where(AiQualityImprovementActionModel.owner == owner)
|
||||
if priority:
|
||||
statement = statement.where(AiQualityImprovementActionModel.priority == priority)
|
||||
if action_type:
|
||||
statement = statement.where(AiQualityImprovementActionModel.action_type == action_type)
|
||||
if dimension:
|
||||
statement = statement.where(AiQualityImprovementActionModel.dimension == dimension)
|
||||
rows = session.scalars(statement.limit(limit)).all()
|
||||
max_days = max((row.evaluation_days for row in rows), default=14)
|
||||
cutoff = datetime.now(UTC) - timedelta(days=max(max_days - 1, 0))
|
||||
@@ -1376,7 +1406,15 @@ class SqlAlchemyAiPlatformRepository:
|
||||
[_quality_improvement_action_from_model(row) for row in rows],
|
||||
runs,
|
||||
)
|
||||
return _filter_improvement_actions(actions, status=status, limit=limit)
|
||||
return _filter_improvement_actions(
|
||||
actions,
|
||||
status=status,
|
||||
owner=owner,
|
||||
priority=priority,
|
||||
action_type=action_type,
|
||||
dimension=dimension,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
def create_quality_improvement_action(
|
||||
self,
|
||||
@@ -2052,15 +2090,28 @@ def _materialize_improvement_action(
|
||||
current = _calculate_improvement_action_stats(action, runs)
|
||||
baseline_rate = _safe_rate(action.baseline_affected_runs, action.baseline_total_runs)
|
||||
current_rate = _safe_rate(current["affected_runs"], current["total_runs"])
|
||||
affected_rate_delta = round(current_rate - baseline_rate, 4)
|
||||
pending_labels_delta = current["pending_labels"] - action.baseline_pending_labels
|
||||
quality_score_delta = round(current["quality_score"] - action.baseline_quality_score, 2)
|
||||
effect_status, effect_summary = _summarize_improvement_effect(
|
||||
action,
|
||||
current_total_runs=current["total_runs"],
|
||||
current_rate=current_rate,
|
||||
affected_rate_delta=affected_rate_delta,
|
||||
pending_labels_delta=pending_labels_delta,
|
||||
quality_score_delta=quality_score_delta,
|
||||
)
|
||||
return action.model_copy(
|
||||
update={
|
||||
"current_total_runs": current["total_runs"],
|
||||
"current_affected_runs": current["affected_runs"],
|
||||
"current_pending_labels": current["pending_labels"],
|
||||
"current_quality_score": current["quality_score"],
|
||||
"affected_rate_delta": round(current_rate - baseline_rate, 4),
|
||||
"pending_labels_delta": current["pending_labels"] - action.baseline_pending_labels,
|
||||
"quality_score_delta": round(current["quality_score"] - action.baseline_quality_score, 2),
|
||||
"affected_rate_delta": affected_rate_delta,
|
||||
"pending_labels_delta": pending_labels_delta,
|
||||
"quality_score_delta": quality_score_delta,
|
||||
"effect_status": effect_status,
|
||||
"effect_summary": effect_summary,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2069,9 +2120,21 @@ def _filter_improvement_actions(
|
||||
actions: list[AiQualityImprovementAction],
|
||||
*,
|
||||
status: str | None = None,
|
||||
owner: str | None = None,
|
||||
priority: str | None = None,
|
||||
action_type: str | None = None,
|
||||
dimension: str | None = None,
|
||||
limit: int = 20,
|
||||
) -> list[AiQualityImprovementAction]:
|
||||
filtered = [action for action in actions if status is None or action.status == status]
|
||||
filtered = [
|
||||
action
|
||||
for action in actions
|
||||
if (status is None or action.status == status)
|
||||
and (owner is None or action.owner == owner)
|
||||
and (priority is None or action.priority == priority)
|
||||
and (action_type is None or action.action_type == action_type)
|
||||
and (dimension is None or action.dimension == dimension)
|
||||
]
|
||||
status_order = {"open": 0, "in_progress": 1, "resolved": 2, "false_positive": 3, "closed": 4}
|
||||
priority_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
||||
return sorted(
|
||||
@@ -2157,6 +2220,37 @@ def _safe_rate(numerator: int | float, denominator: int | float) -> float:
|
||||
return round(float(numerator) / float(denominator), 4) if denominator else 0.0
|
||||
|
||||
|
||||
def _summarize_improvement_effect(
|
||||
action: AiQualityImprovementAction,
|
||||
*,
|
||||
current_total_runs: int | float,
|
||||
current_rate: float,
|
||||
affected_rate_delta: float,
|
||||
pending_labels_delta: int | float,
|
||||
quality_score_delta: float,
|
||||
) -> tuple[str, str]:
|
||||
baseline_rate = _safe_rate(action.baseline_affected_runs, action.baseline_total_runs)
|
||||
if action.baseline_total_runs <= 0 or current_total_runs <= 0:
|
||||
return "pending_data", "Need more AI run data to evaluate this improvement action."
|
||||
improved = (
|
||||
affected_rate_delta < -0.01
|
||||
or pending_labels_delta < 0
|
||||
or quality_score_delta > 1
|
||||
)
|
||||
regressed = (
|
||||
affected_rate_delta > 0.01
|
||||
or pending_labels_delta > 0
|
||||
or quality_score_delta < -1
|
||||
)
|
||||
status = "improved" if improved and not regressed else "regressed" if regressed and not improved else "unchanged"
|
||||
summary = (
|
||||
f"Baseline affected rate {baseline_rate:.0%}, current {current_rate:.0%}; "
|
||||
f"pending labels delta {pending_labels_delta:+.0f}; "
|
||||
f"quality score delta {quality_score_delta:+.1f}."
|
||||
)
|
||||
return status, summary
|
||||
|
||||
|
||||
def _default_improvement_action_title(action: AiQualityImprovementActionCreate) -> str:
|
||||
action_names = {
|
||||
"investigate": "Investigate",
|
||||
|
||||
@@ -233,10 +233,19 @@ def test_quality_attribution_can_create_and_resolve_improvement_action() -> None
|
||||
assert created["baseline_total_runs"] == 1
|
||||
assert created["baseline_affected_runs"] == 1
|
||||
assert created["current_affected_runs"] == 1
|
||||
assert created["effect_status"] == "unchanged"
|
||||
assert "Baseline affected rate" in created["effect_summary"]
|
||||
|
||||
list_response = client.get(
|
||||
"/api/v1/audit/quality-improvement-actions",
|
||||
params={"status": "open", "limit": 10},
|
||||
params={
|
||||
"status": "open",
|
||||
"owner": "admin",
|
||||
"priority": "high",
|
||||
"action_type": "tune_model",
|
||||
"dimension": "model",
|
||||
"limit": 10,
|
||||
},
|
||||
)
|
||||
assert list_response.status_code == 200
|
||||
assert list_response.json()[0]["id"] == created["id"]
|
||||
@@ -257,9 +266,12 @@ def test_quality_attribution_can_create_and_resolve_improvement_action() -> None
|
||||
resolved = resolved_response.json()
|
||||
assert resolved["status"] == "resolved"
|
||||
assert resolved["resolved_at"]
|
||||
assert resolved["effect_summary"]
|
||||
|
||||
updated_dashboard = client.get("/api/v1/audit/quality-dashboard", params={"days": 7}).json()
|
||||
assert any(action["id"] == created["id"] for action in updated_dashboard["improvement_actions"])
|
||||
dashboard_action = next(action for action in updated_dashboard["improvement_actions"] if action["id"] == created["id"])
|
||||
assert dashboard_action["effect_status"] in {"unchanged", "improved", "regressed", "pending_data"}
|
||||
assert dashboard_action["effect_summary"]
|
||||
|
||||
|
||||
def test_knowledge_document_can_be_added_and_searched() -> None:
|
||||
|
||||
@@ -358,6 +358,8 @@ class AiQualityImprovementAction(AiQualityImprovementActionCreate):
|
||||
affected_rate_delta: float = 0.0
|
||||
pending_labels_delta: int = 0
|
||||
quality_score_delta: float = 0.0
|
||||
effect_status: str = "pending_data"
|
||||
effect_summary: str | None = None
|
||||
resolution_note: str | None = None
|
||||
created_at: str
|
||||
updated_at: str
|
||||
@@ -653,12 +655,24 @@ class AiPlatformClient:
|
||||
self,
|
||||
*,
|
||||
status: str | None = None,
|
||||
owner: str | None = None,
|
||||
priority: str | None = None,
|
||||
action_type: str | None = None,
|
||||
dimension: str | None = None,
|
||||
limit: int = 20,
|
||||
context: RequestContext | None = None,
|
||||
) -> list[AiQualityImprovementAction]:
|
||||
params: dict[str, Any] = {"limit": limit}
|
||||
if status:
|
||||
params["status"] = status
|
||||
if owner:
|
||||
params["owner"] = owner
|
||||
if priority:
|
||||
params["priority"] = priority
|
||||
if action_type:
|
||||
params["action_type"] = action_type
|
||||
if dimension:
|
||||
params["dimension"] = dimension
|
||||
response = self._client.get(
|
||||
f"{self._api_prefix}/audit/quality-improvement-actions",
|
||||
params=params,
|
||||
|
||||
@@ -125,6 +125,8 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
|
||||
"affected_rate_delta": 0.0,
|
||||
"pending_labels_delta": 0,
|
||||
"quality_score_delta": 0.0,
|
||||
"effect_status": "unchanged",
|
||||
"effect_summary": "Baseline affected rate 100%, current 100%; pending labels delta +0; quality score delta +0.0.",
|
||||
"resolution_note": resolution_note,
|
||||
"created_at": "2026-06-22T00:00:00Z",
|
||||
"updated_at": "2026-06-22T00:02:00Z",
|
||||
@@ -581,6 +583,10 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
|
||||
if request.url.path == "/api/v1/audit/quality-improvement-actions":
|
||||
query = parse_qs(request.url.query.decode())
|
||||
assert query["status"] == ["open"]
|
||||
assert query["owner"] == ["admin"]
|
||||
assert query["priority"] == ["high"]
|
||||
assert query["action_type"] == ["tune_model"]
|
||||
assert query["dimension"] == ["model"]
|
||||
assert query["limit"] == ["5"]
|
||||
return httpx.Response(200, json=[quality_action_payload()])
|
||||
if request.url.path == "/api/v1/audit/quality-improvement-actions/qa-action-1":
|
||||
@@ -749,8 +755,17 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
|
||||
)
|
||||
assert created_action.status == "open"
|
||||
assert created_action.baseline_affected_runs == 1
|
||||
listed_actions = client.list_quality_improvement_actions(status="open", limit=5)
|
||||
assert created_action.effect_status == "unchanged"
|
||||
listed_actions = client.list_quality_improvement_actions(
|
||||
status="open",
|
||||
owner="admin",
|
||||
priority="high",
|
||||
action_type="tune_model",
|
||||
dimension="model",
|
||||
limit=5,
|
||||
)
|
||||
assert listed_actions[0].id == "qa-action-1"
|
||||
assert listed_actions[0].effect_summary is not None
|
||||
updated_action = client.update_quality_improvement_action(
|
||||
"qa-action-1",
|
||||
AiQualityImprovementActionUpdate(
|
||||
|
||||
+88
-1
@@ -96,6 +96,8 @@ type QualityImprovementAction = {
|
||||
affected_rate_delta: number;
|
||||
pending_labels_delta: number;
|
||||
quality_score_delta: number;
|
||||
effect_status?: string;
|
||||
effect_summary?: string | null;
|
||||
resolution_note?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -115,6 +117,14 @@ type LoadState =
|
||||
| { status: "ready"; dashboard: QualityDashboard }
|
||||
| { status: "error"; message: string };
|
||||
|
||||
type ActionFilters = {
|
||||
status: string;
|
||||
owner: string;
|
||||
priority: string;
|
||||
actionType: string;
|
||||
dimension: string;
|
||||
};
|
||||
|
||||
const severityTone: Record<string, string> = {
|
||||
info: "tag-primary",
|
||||
warning: "tag-warning",
|
||||
@@ -152,11 +162,32 @@ const dimensionLabels: Record<string, string> = {
|
||||
label_code: "标签"
|
||||
};
|
||||
|
||||
const effectTone: Record<string, string> = {
|
||||
improved: "tag-success",
|
||||
unchanged: "tag-warning",
|
||||
regressed: "tag-danger",
|
||||
pending_data: ""
|
||||
};
|
||||
|
||||
const effectLabels: Record<string, string> = {
|
||||
improved: "已改善",
|
||||
unchanged: "未明显变化",
|
||||
regressed: "有恶化",
|
||||
pending_data: "待观察"
|
||||
};
|
||||
|
||||
export default function AiQualityDashboardPage() {
|
||||
const [state, setState] = useState<LoadState>({ status: "loading" });
|
||||
const [days, setDays] = useState(14);
|
||||
const [actionMessage, setActionMessage] = useState<string | null>(null);
|
||||
const [actionBusyKey, setActionBusyKey] = useState<string | null>(null);
|
||||
const [filters, setFilters] = useState<ActionFilters>({
|
||||
status: "",
|
||||
owner: "",
|
||||
priority: "",
|
||||
actionType: "",
|
||||
dimension: ""
|
||||
});
|
||||
|
||||
async function loadDashboard(nextDays = days) {
|
||||
setState({ status: "loading" });
|
||||
@@ -185,7 +216,9 @@ export default function AiQualityDashboardPage() {
|
||||
|
||||
const dashboard = state.status === "ready" ? state.dashboard : null;
|
||||
const improvementActions = dashboard?.improvement_actions ?? [];
|
||||
const filteredActions = filterImprovementActions(improvementActions, filters);
|
||||
const openActionCount = improvementActions.filter((action) => ["open", "in_progress"].includes(action.status)).length;
|
||||
const ownerOptions = Array.from(new Set(improvementActions.map((action) => action.owner).filter(Boolean))).sort() as string[];
|
||||
|
||||
function changeDays(value: string) {
|
||||
const nextDays = Number(value);
|
||||
@@ -194,6 +227,10 @@ export default function AiQualityDashboardPage() {
|
||||
void loadDashboard(nextDays);
|
||||
}
|
||||
|
||||
function updateFilter(key: keyof ActionFilters, value: string) {
|
||||
setFilters((current) => ({ ...current, [key]: value }));
|
||||
}
|
||||
|
||||
async function createActionFromAttribution(item: QualityAttributionItem) {
|
||||
const busyKey = attributionBusyKey(item);
|
||||
setActionBusyKey(busyKey);
|
||||
@@ -331,11 +368,46 @@ export default function AiQualityDashboardPage() {
|
||||
<span className="tag tag-primary">待推进 {openActionCount}</span>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<div className="quality-action-filters">
|
||||
<select className="search-input select-input-compact" value={filters.status} onChange={(event) => updateFilter("status", event.target.value)}>
|
||||
<option value="">全部状态</option>
|
||||
{Object.entries(statusLabels).map(([value, label]) => (
|
||||
<option key={value} value={value}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="search-input select-input-compact" value={filters.owner} onChange={(event) => updateFilter("owner", event.target.value)}>
|
||||
<option value="">全部负责人</option>
|
||||
{ownerOptions.map((owner) => (
|
||||
<option key={owner} value={owner}>{owner}</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="search-input select-input-compact" value={filters.priority} onChange={(event) => updateFilter("priority", event.target.value)}>
|
||||
<option value="">全部优先级</option>
|
||||
<option value="critical">critical</option>
|
||||
<option value="high">high</option>
|
||||
<option value="medium">medium</option>
|
||||
<option value="low">low</option>
|
||||
</select>
|
||||
<select className="search-input select-input-compact" value={filters.actionType} onChange={(event) => updateFilter("actionType", event.target.value)}>
|
||||
<option value="">全部动作</option>
|
||||
{Object.entries(actionTypeLabels).map(([value, label]) => (
|
||||
<option key={value} value={value}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="search-input select-input-compact" value={filters.dimension} onChange={(event) => updateFilter("dimension", event.target.value)}>
|
||||
<option value="">全部维度</option>
|
||||
{Object.entries(dimensionLabels).map(([value, label]) => (
|
||||
<option key={value} value={value}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{improvementActions.length === 0 ? (
|
||||
<div className="empty-state empty-state-compact">暂无改进项,可先从上方归因卡片生成。</div>
|
||||
) : filteredActions.length === 0 ? (
|
||||
<div className="empty-state empty-state-compact">当前筛选条件下没有改进项。</div>
|
||||
) : (
|
||||
<div className="quality-action-list">
|
||||
{improvementActions.map((action) => (
|
||||
{filteredActions.map((action) => (
|
||||
<QualityActionItem
|
||||
action={action}
|
||||
busyKey={actionBusyKey}
|
||||
@@ -499,6 +571,7 @@ function QualityActionItem({
|
||||
const busy = busyKey?.startsWith(`${action.id}:`) ?? false;
|
||||
const baselineRate = safeRate(action.baseline_affected_runs, action.baseline_total_runs);
|
||||
const currentRate = safeRate(action.current_affected_runs, action.current_total_runs);
|
||||
const effectStatus = action.effect_status ?? "pending_data";
|
||||
|
||||
return (
|
||||
<div className="quality-action-item">
|
||||
@@ -512,8 +585,11 @@ function QualityActionItem({
|
||||
<span>{action.owner || "未分配"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="action-row">
|
||||
<span className={`tag ${effectTone[effectStatus] ?? ""}`}>{effectLabels[effectStatus] ?? effectStatus}</span>
|
||||
<span className={`tag ${statusTone[action.status] ?? ""}`}>{statusLabels[action.status] ?? action.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
{(action.description || action.root_cause_hint) && (
|
||||
<span className="item-meta">{action.description || action.root_cause_hint}</span>
|
||||
)}
|
||||
@@ -524,6 +600,7 @@ function QualityActionItem({
|
||||
<span>变化 {formatSignedPercent(action.affected_rate_delta)}</span>
|
||||
</div>
|
||||
{action.resolution_note && <span className="item-meta">处理说明:{action.resolution_note}</span>}
|
||||
{action.effect_summary && <span className="item-meta">效果复盘:{action.effect_summary}</span>}
|
||||
<div className="action-row">
|
||||
{action.status === "open" && (
|
||||
<button className="button button-soft" disabled={busy} onClick={() => void onUpdate(action, "in_progress")} type="button">
|
||||
@@ -548,6 +625,16 @@ function QualityActionItem({
|
||||
);
|
||||
}
|
||||
|
||||
function filterImprovementActions(actions: QualityImprovementAction[], filters: ActionFilters) {
|
||||
return actions.filter((action) => (
|
||||
(!filters.status || action.status === filters.status)
|
||||
&& (!filters.owner || action.owner === filters.owner)
|
||||
&& (!filters.priority || action.priority === filters.priority)
|
||||
&& (!filters.actionType || action.action_type === filters.actionType)
|
||||
&& (!filters.dimension || action.dimension === filters.dimension)
|
||||
));
|
||||
}
|
||||
|
||||
function inferActionType(item: QualityAttributionItem) {
|
||||
if (item.dimension === "prompt_template") {
|
||||
return "tune_prompt";
|
||||
|
||||
+5
-3
@@ -7,12 +7,14 @@ const aiPlatformBaseUrl =
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const status = searchParams.get("status");
|
||||
const limit = searchParams.get("limit") ?? "20";
|
||||
const targetUrl = new URL("/api/v1/audit/quality-improvement-actions", aiPlatformBaseUrl);
|
||||
targetUrl.searchParams.set("limit", limit);
|
||||
if (status) {
|
||||
targetUrl.searchParams.set("status", status);
|
||||
for (const name of ["status", "owner", "priority", "action_type", "dimension"]) {
|
||||
const value = searchParams.get(name);
|
||||
if (value) {
|
||||
targetUrl.searchParams.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -743,6 +743,17 @@ select {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quality-action-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.quality-action-filters .search-input {
|
||||
min-width: 128px;
|
||||
}
|
||||
|
||||
.quality-action-item {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
Reference in New Issue
Block a user