feat: execute ai quality improvement actions
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# V2 AI 质量改进动作执行闭环记录 - 2026-06-23
|
||||
|
||||
## 路线核查
|
||||
|
||||
本轮继续对照《AI 合同平台 V2 重构版开发方案》推进,没有跑偏:
|
||||
|
||||
- AI 质量归因、Prompt、知识库、审计仍归属 `yuqei-ai-platform`。
|
||||
- 合同侧前端只通过 `/api/ai-platform/*` 代理调用 AI 中台,不直接拼模型请求、不直接操作 AI 内部仓库。
|
||||
- 旧 `shared-*` 仓库没有新增能力,也没有恢复 vendored SDK。
|
||||
- 本轮仍围绕 “AI 中台质量评测 -> 归因 -> 改进动作 -> 真实变更 -> 效果回看” 主线推进。
|
||||
|
||||
## 本轮完成内容
|
||||
|
||||
### 1. 改进项完成前校验
|
||||
|
||||
AI Platform 后端新增改进项完成校验:
|
||||
|
||||
- `tune_prompt` 类型必须关联 `linked_prompt_template_id` 和 `linked_prompt_template_version` 后才能标记 `resolved`。
|
||||
- `improve_knowledge` 类型必须关联 `linked_knowledge_base_id` 和 `linked_import_batch_id` 后才能标记 `resolved`。
|
||||
- 缺少真实变更对象时,API 返回 `422` 和明确原因,避免“纸面完成”。
|
||||
|
||||
### 2. Prompt 改进执行入口
|
||||
|
||||
质量运营看板的 Prompt 类改进项新增:
|
||||
|
||||
- “生成 Prompt 草稿”入口。
|
||||
- 自动基于当前 Prompt 或默认模板生成新草稿。
|
||||
- 自动把新草稿的 Prompt ID/版本回填到改进项。
|
||||
- 管理员可继续进入 Prompt 中心编辑、发布和观察后续指标。
|
||||
|
||||
### 3. 知识库改进执行入口
|
||||
|
||||
质量运营看板的知识库类改进项新增:
|
||||
|
||||
- 轻量补知识库表单。
|
||||
- 创建知识库导入批次。
|
||||
- 写入知识文档。
|
||||
- 自动把知识库 ID 和导入批次 ID 回填到改进项。
|
||||
|
||||
### 4. 前端代理补齐
|
||||
|
||||
合同侧前端新增/增强 AI Platform 代理:
|
||||
|
||||
- `POST /api/ai-platform/knowledge-import-batches`
|
||||
- `POST /api/ai-platform/knowledge-documents`
|
||||
|
||||
这些代理只承担管理后台调用转发,不改变 AI 能力归属边界。
|
||||
|
||||
## 闭环价值
|
||||
|
||||
本轮之后,质量运营链路从:
|
||||
|
||||
```text
|
||||
发现问题 -> 生成改进项 -> 手工标记完成
|
||||
```
|
||||
|
||||
升级为:
|
||||
|
||||
```text
|
||||
发现问题 -> 生成改进项 -> 生成 Prompt 草稿 / 补知识库 -> 挂真实变更 -> 再标记完成 -> 后续指标回看
|
||||
```
|
||||
|
||||
这样 `调整 Prompt`、`补知识库` 不再只是文字状态,而是能追溯到真实配置变更记录。
|
||||
|
||||
## 验证重点
|
||||
|
||||
- 没有真实 Prompt/知识库关联时,改进项不能标记完成。
|
||||
- Prompt 改进项能生成草稿并回填关联。
|
||||
- 知识库改进项能创建导入批次、写入文档并回填关联。
|
||||
- 质量看板仍能加载、筛选、更新改进项状态。
|
||||
|
||||
## 下一步建议
|
||||
|
||||
下一步可以进入 “改进效果复盘增强”:
|
||||
|
||||
- 将改进项完成时间作为效果观察窗口起点。
|
||||
- 在质量看板中展示完成前后异常率对比。
|
||||
- 针对同一个 Prompt/知识库变更聚合关联的多个改进项。
|
||||
- 给 Prompt 发布、知识库导入增加更明确的变更审计事件。
|
||||
@@ -321,6 +321,8 @@ def create_app(
|
||||
return store.update_quality_improvement_action(action_id, payload)
|
||||
except KeyError as error:
|
||||
raise HTTPException(status_code=404, detail="AI quality improvement action not found.") from error
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=422, detail=str(error)) from error
|
||||
|
||||
@app.patch(
|
||||
f"{resolved_settings.api_prefix}/audit/ai-runs/{{run_id}}/quality-labels/{{label_code}}",
|
||||
|
||||
@@ -908,6 +908,16 @@ class InMemoryAiPlatformRepository:
|
||||
action = self.quality_improvement_actions.get(action_id)
|
||||
if action is None:
|
||||
raise KeyError(action_id)
|
||||
_validate_quality_action_completion(
|
||||
action_type=action.action_type,
|
||||
status=payload.status,
|
||||
linked_prompt_template_id=payload.linked_prompt_template_id or action.linked_prompt_template_id,
|
||||
linked_prompt_template_version=(
|
||||
payload.linked_prompt_template_version or action.linked_prompt_template_version
|
||||
),
|
||||
linked_knowledge_base_id=payload.linked_knowledge_base_id or action.linked_knowledge_base_id,
|
||||
linked_import_batch_id=payload.linked_import_batch_id or action.linked_import_batch_id,
|
||||
)
|
||||
update: dict[str, object] = {
|
||||
"status": payload.status,
|
||||
"updated_at": now,
|
||||
@@ -1684,6 +1694,16 @@ class SqlAlchemyAiPlatformRepository:
|
||||
row = session.get(AiQualityImprovementActionModel, action_id)
|
||||
if row is None:
|
||||
raise KeyError(action_id)
|
||||
_validate_quality_action_completion(
|
||||
action_type=row.action_type,
|
||||
status=payload.status,
|
||||
linked_prompt_template_id=payload.linked_prompt_template_id or row.linked_prompt_template_id,
|
||||
linked_prompt_template_version=(
|
||||
payload.linked_prompt_template_version or row.linked_prompt_template_version
|
||||
),
|
||||
linked_knowledge_base_id=payload.linked_knowledge_base_id or row.linked_knowledge_base_id,
|
||||
linked_import_batch_id=payload.linked_import_batch_id or row.linked_import_batch_id,
|
||||
)
|
||||
row.status = payload.status
|
||||
if payload.owner is not None:
|
||||
row.owner = payload.owner
|
||||
@@ -2032,6 +2052,29 @@ def _with_inferred_action_links(
|
||||
return payload.model_copy(update=update) if update else payload
|
||||
|
||||
|
||||
def _validate_quality_action_completion(
|
||||
*,
|
||||
action_type: str,
|
||||
status: str,
|
||||
linked_prompt_template_id: str | None,
|
||||
linked_prompt_template_version: str | None,
|
||||
linked_knowledge_base_id: str | None,
|
||||
linked_import_batch_id: str | None,
|
||||
) -> None:
|
||||
if status != "resolved":
|
||||
return
|
||||
if action_type == "tune_prompt" and not (linked_prompt_template_id and linked_prompt_template_version):
|
||||
raise ValueError(
|
||||
"Prompt improvement actions require linked_prompt_template_id and "
|
||||
"linked_prompt_template_version before they can be resolved."
|
||||
)
|
||||
if action_type == "improve_knowledge" and not (linked_knowledge_base_id and linked_import_batch_id):
|
||||
raise ValueError(
|
||||
"Knowledge improvement actions require linked_knowledge_base_id and "
|
||||
"linked_import_batch_id before they can be resolved."
|
||||
)
|
||||
|
||||
|
||||
def _find_prompt_template_link(
|
||||
key: str,
|
||||
prompt_templates: Iterable[PromptTemplate],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from yuqei_ai_platform_api.config import AiPlatformSettings
|
||||
@@ -373,6 +374,82 @@ def test_quality_improvement_action_links_prompt_and_knowledge_changes() -> None
|
||||
assert updated.linked_import_batch_id == "kb-batch-manual"
|
||||
|
||||
|
||||
def test_quality_improvement_action_requires_real_change_before_resolved() -> None:
|
||||
repository = InMemoryAiPlatformRepository()
|
||||
|
||||
prompt_action = repository.create_quality_improvement_action(
|
||||
AiQualityImprovementActionCreate(
|
||||
dimension="label_code",
|
||||
key="prompt_missing_citation",
|
||||
label="Prompt missing citation",
|
||||
action_type="tune_prompt",
|
||||
evaluation_days=7,
|
||||
)
|
||||
)
|
||||
with pytest.raises(ValueError, match="Prompt improvement actions require"):
|
||||
repository.update_quality_improvement_action(
|
||||
prompt_action.id,
|
||||
AiQualityImprovementActionUpdate(status="resolved"),
|
||||
)
|
||||
|
||||
knowledge_action = repository.create_quality_improvement_action(
|
||||
AiQualityImprovementActionCreate(
|
||||
dimension="label_code",
|
||||
key="source_limited",
|
||||
label="Source limited",
|
||||
action_type="improve_knowledge",
|
||||
evaluation_days=7,
|
||||
)
|
||||
)
|
||||
with pytest.raises(ValueError, match="Knowledge improvement actions require"):
|
||||
repository.update_quality_improvement_action(
|
||||
knowledge_action.id,
|
||||
AiQualityImprovementActionUpdate(status="resolved"),
|
||||
)
|
||||
|
||||
batch = repository.create_knowledge_import_batch(
|
||||
KnowledgeImportBatchCreate(
|
||||
knowledge_base_id="default",
|
||||
source_name="quality-action-source-fix",
|
||||
source_type="quality_improvement",
|
||||
)
|
||||
)
|
||||
resolved = repository.update_quality_improvement_action(
|
||||
knowledge_action.id,
|
||||
AiQualityImprovementActionUpdate(
|
||||
status="resolved",
|
||||
linked_knowledge_base_id="default",
|
||||
linked_import_batch_id=batch.id,
|
||||
resolution_note="Knowledge base was supplemented.",
|
||||
),
|
||||
)
|
||||
assert resolved.status == "resolved"
|
||||
assert resolved.linked_import_batch_id == batch.id
|
||||
|
||||
|
||||
def test_quality_improvement_action_resolve_api_returns_422_without_real_change() -> None:
|
||||
client = make_client()
|
||||
create_response = client.post(
|
||||
"/api/v1/audit/quality-improvement-actions",
|
||||
json={
|
||||
"dimension": "label_code",
|
||||
"key": "source_limited",
|
||||
"label": "Source limited",
|
||||
"action_type": "improve_knowledge",
|
||||
"evaluation_days": 7,
|
||||
},
|
||||
)
|
||||
assert create_response.status_code == 200
|
||||
action = create_response.json()
|
||||
|
||||
resolve_response = client.patch(
|
||||
f"/api/v1/audit/quality-improvement-actions/{action['id']}",
|
||||
json={"status": "resolved"},
|
||||
)
|
||||
assert resolve_response.status_code == 422
|
||||
assert "linked_knowledge_base_id" in resolve_response.json()["detail"]
|
||||
|
||||
|
||||
def test_knowledge_document_can_be_added_and_searched() -> None:
|
||||
client = make_client()
|
||||
|
||||
|
||||
+305
-3
@@ -5,9 +5,12 @@ import type { ReactNode } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
AlertTriangle,
|
||||
BookOpen,
|
||||
CheckCircle2,
|
||||
Clock3,
|
||||
Database,
|
||||
FilePlus2,
|
||||
PencilLine,
|
||||
PlayCircle,
|
||||
RefreshCcw,
|
||||
ShieldCheck,
|
||||
@@ -120,6 +123,7 @@ type PromptTemplateOption = {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
content?: string;
|
||||
status: string;
|
||||
published_at?: string | null;
|
||||
updated_at: string;
|
||||
@@ -147,6 +151,13 @@ type ActionFilters = {
|
||||
dimension: string;
|
||||
};
|
||||
|
||||
type KnowledgeDraft = {
|
||||
sourceName: string;
|
||||
title: string;
|
||||
reference: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
const severityTone: Record<string, string> = {
|
||||
info: "tag-primary",
|
||||
warning: "tag-warning",
|
||||
@@ -205,6 +216,7 @@ export default function AiQualityDashboardPage() {
|
||||
const [actionBusyKey, setActionBusyKey] = useState<string | null>(null);
|
||||
const [promptOptions, setPromptOptions] = useState<PromptTemplateOption[]>([]);
|
||||
const [importBatchOptions, setImportBatchOptions] = useState<KnowledgeImportBatchOption[]>([]);
|
||||
const [knowledgeDrafts, setKnowledgeDrafts] = useState<Record<string, KnowledgeDraft>>({});
|
||||
const [filters, setFilters] = useState<ActionFilters>({
|
||||
status: "",
|
||||
owner: "",
|
||||
@@ -295,7 +307,7 @@ export default function AiQualityDashboardPage() {
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.message ?? `HTTP ${response.status}`);
|
||||
throw new Error(readApiError(payload, response.status));
|
||||
}
|
||||
setActionMessage("已生成改进项,可以在下方队列继续处理。");
|
||||
await loadLinkOptions();
|
||||
@@ -334,6 +346,139 @@ export default function AiQualityDashboardPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function createPromptDraftForAction(action: QualityImprovementAction) {
|
||||
const busyKey = actionBusyKeyFor(action, "prompt-draft");
|
||||
setActionBusyKey(busyKey);
|
||||
setActionMessage(null);
|
||||
try {
|
||||
const sourcePrompt = findPromptOption(action.linked_prompt_template_id || action.key, promptOptions);
|
||||
const promptName = sourcePrompt?.name || (action.key.includes(":") ? action.key.split(":")[0] : "legal_qa.default");
|
||||
const version = `quality-${new Date().toISOString().slice(0, 10).replaceAll("-", "")}-${action.id.slice(-4)}`;
|
||||
const response = await fetch("/api/ai-platform/prompt-templates", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: promptName,
|
||||
version,
|
||||
content: buildPromptDraftContent(action, sourcePrompt?.content),
|
||||
enabled: false
|
||||
})
|
||||
});
|
||||
const prompt = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new Error(readApiError(prompt, response.status));
|
||||
}
|
||||
|
||||
const updateResponse = await fetch(`/api/ai-platform/audit/quality-improvement-actions/${encodeURIComponent(action.id)}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
status: "in_progress",
|
||||
owner: "admin",
|
||||
linked_prompt_template_id: prompt.id,
|
||||
linked_prompt_template_version: prompt.version,
|
||||
resolution_note: "已生成 Prompt 草稿,待编辑发布并继续观察质量指标。"
|
||||
})
|
||||
});
|
||||
const updatePayload = await updateResponse.json().catch(() => null);
|
||||
if (!updateResponse.ok) {
|
||||
throw new Error(readApiError(updatePayload, updateResponse.status));
|
||||
}
|
||||
|
||||
setActionMessage(`已生成 Prompt 草稿 ${prompt.name} / ${prompt.version},可进入 Prompt 中心编辑发布。`);
|
||||
await loadLinkOptions();
|
||||
await loadDashboard(days);
|
||||
} catch (error) {
|
||||
setActionMessage(error instanceof Error ? error.message : "生成 Prompt 草稿失败");
|
||||
} finally {
|
||||
setActionBusyKey(null);
|
||||
}
|
||||
}
|
||||
|
||||
function updateKnowledgeDraft(action: QualityImprovementAction, patch: Partial<KnowledgeDraft>) {
|
||||
setKnowledgeDrafts((current) => ({
|
||||
...current,
|
||||
[action.id]: {
|
||||
...defaultKnowledgeDraft(action),
|
||||
...current[action.id],
|
||||
...patch
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async function submitKnowledgeDraftForAction(action: QualityImprovementAction) {
|
||||
const busyKey = actionBusyKeyFor(action, "knowledge-import");
|
||||
const draft = knowledgeDrafts[action.id] ?? defaultKnowledgeDraft(action);
|
||||
const knowledgeBaseId = action.linked_knowledge_base_id || (action.dimension === "knowledge_base" ? action.key : "default");
|
||||
if (!draft.title.trim() || !draft.content.trim()) {
|
||||
setActionMessage("请先填写知识标题和补充内容,再创建导入批次。");
|
||||
return;
|
||||
}
|
||||
setActionBusyKey(busyKey);
|
||||
setActionMessage(null);
|
||||
try {
|
||||
const batchResponse = await fetch("/api/ai-platform/knowledge-import-batches", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
knowledge_base_id: knowledgeBaseId,
|
||||
source_name: draft.sourceName.trim() || `quality-action-${action.id}`,
|
||||
source_type: "quality_improvement"
|
||||
})
|
||||
});
|
||||
const batch = await batchResponse.json().catch(() => null);
|
||||
if (!batchResponse.ok) {
|
||||
throw new Error(readApiError(batch, batchResponse.status));
|
||||
}
|
||||
|
||||
const documentResponse = await fetch("/api/ai-platform/knowledge-documents", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
import_batch_id: batch.id,
|
||||
knowledge_base_id: knowledgeBaseId,
|
||||
title: draft.title.trim(),
|
||||
source_type: "quality_improvement",
|
||||
reference: draft.reference.trim() || action.label,
|
||||
content: draft.content.trim()
|
||||
})
|
||||
});
|
||||
const documentPayload = await documentResponse.json().catch(() => null);
|
||||
if (!documentResponse.ok) {
|
||||
throw new Error(readApiError(documentPayload, documentResponse.status));
|
||||
}
|
||||
|
||||
const updateResponse = await fetch(`/api/ai-platform/audit/quality-improvement-actions/${encodeURIComponent(action.id)}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
status: "in_progress",
|
||||
owner: "admin",
|
||||
linked_knowledge_base_id: knowledgeBaseId,
|
||||
linked_import_batch_id: batch.id,
|
||||
resolution_note: "已补充知识库导入批次,待后续 AI 调用验证来源不足率。"
|
||||
})
|
||||
});
|
||||
const updatePayload = await updateResponse.json().catch(() => null);
|
||||
if (!updateResponse.ok) {
|
||||
throw new Error(readApiError(updatePayload, updateResponse.status));
|
||||
}
|
||||
|
||||
setKnowledgeDrafts((current) => {
|
||||
const next = { ...current };
|
||||
delete next[action.id];
|
||||
return next;
|
||||
});
|
||||
setActionMessage(`已写入知识库 ${knowledgeBaseId},导入批次 ${batch.id} 已关联到改进项。`);
|
||||
await loadLinkOptions();
|
||||
await loadDashboard(days);
|
||||
} catch (error) {
|
||||
setActionMessage(error instanceof Error ? error.message : "补充知识库失败");
|
||||
} finally {
|
||||
setActionBusyKey(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="page-heading">
|
||||
@@ -453,8 +598,12 @@ export default function AiQualityDashboardPage() {
|
||||
<QualityActionItem
|
||||
action={action}
|
||||
busyKey={actionBusyKey}
|
||||
knowledgeDraft={knowledgeDrafts[action.id] ?? defaultKnowledgeDraft(action)}
|
||||
key={action.id}
|
||||
onCreatePromptDraft={createPromptDraftForAction}
|
||||
onSubmitKnowledgeDraft={submitKnowledgeDraftForAction}
|
||||
onUpdate={updateAction}
|
||||
onUpdateKnowledgeDraft={updateKnowledgeDraft}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -604,16 +753,27 @@ function AttributionPanel({
|
||||
function QualityActionItem({
|
||||
action,
|
||||
busyKey,
|
||||
onUpdate
|
||||
knowledgeDraft,
|
||||
onCreatePromptDraft,
|
||||
onSubmitKnowledgeDraft,
|
||||
onUpdate,
|
||||
onUpdateKnowledgeDraft
|
||||
}: {
|
||||
action: QualityImprovementAction;
|
||||
busyKey: string | null;
|
||||
knowledgeDraft: KnowledgeDraft;
|
||||
onCreatePromptDraft: (action: QualityImprovementAction) => void | Promise<void>;
|
||||
onSubmitKnowledgeDraft: (action: QualityImprovementAction) => void | Promise<void>;
|
||||
onUpdate: (action: QualityImprovementAction, status: string) => void | Promise<void>;
|
||||
onUpdateKnowledgeDraft: (action: QualityImprovementAction, patch: Partial<KnowledgeDraft>) => void;
|
||||
}) {
|
||||
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";
|
||||
const needsPromptDraft = action.action_type === "tune_prompt";
|
||||
const needsKnowledgeImport = action.action_type === "improve_knowledge";
|
||||
const completionBlockReason = getCompletionBlockReason(action);
|
||||
|
||||
return (
|
||||
<div className="quality-action-item">
|
||||
@@ -644,12 +804,81 @@ function QualityActionItem({
|
||||
{action.resolution_note && <span className="item-meta">处理说明:{action.resolution_note}</span>}
|
||||
<LinkedChangeSummary action={action} />
|
||||
{action.effect_summary && <span className="item-meta">效果复盘:{action.effect_summary}</span>}
|
||||
{["open", "in_progress"].includes(action.status) && (needsPromptDraft || needsKnowledgeImport) && (
|
||||
<div className="quality-action-execution">
|
||||
{needsPromptDraft && (
|
||||
<button className="button button-soft" disabled={busy} onClick={() => void onCreatePromptDraft(action)} type="button">
|
||||
<PencilLine size={16} />
|
||||
{busyKey === actionBusyKeyFor(action, "prompt-draft") ? "生成中" : "生成 Prompt 草稿"}
|
||||
</button>
|
||||
)}
|
||||
{needsKnowledgeImport && (
|
||||
<div className="quality-action-mini-form">
|
||||
<div className="settings-grid">
|
||||
<label className="setting-field">
|
||||
<span>知识库</span>
|
||||
<input
|
||||
className="search-input"
|
||||
readOnly
|
||||
value={action.linked_knowledge_base_id || (action.dimension === "knowledge_base" ? action.key : "default")}
|
||||
/>
|
||||
</label>
|
||||
<label className="setting-field">
|
||||
<span>来源名称</span>
|
||||
<input
|
||||
className="search-input"
|
||||
value={knowledgeDraft.sourceName}
|
||||
onChange={(event) => onUpdateKnowledgeDraft(action, { sourceName: event.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="setting-field">
|
||||
<span>标题</span>
|
||||
<input
|
||||
className="search-input"
|
||||
value={knowledgeDraft.title}
|
||||
onChange={(event) => onUpdateKnowledgeDraft(action, { title: event.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="setting-field">
|
||||
<span>依据/定位</span>
|
||||
<input
|
||||
className="search-input"
|
||||
value={knowledgeDraft.reference}
|
||||
onChange={(event) => onUpdateKnowledgeDraft(action, { reference: event.target.value })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label className="setting-field">
|
||||
<span>补充内容</span>
|
||||
<textarea
|
||||
className="search-input quality-action-textarea"
|
||||
value={knowledgeDraft.content}
|
||||
onChange={(event) => onUpdateKnowledgeDraft(action, { content: event.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<div className="action-row">
|
||||
<button className="button button-soft" disabled={busy} onClick={() => void onSubmitKnowledgeDraft(action)} type="button">
|
||||
<FilePlus2 size={16} />
|
||||
{busyKey === actionBusyKeyFor(action, "knowledge-import") ? "写入中" : "写入知识库并关联"}
|
||||
</button>
|
||||
<span className="item-meta">会创建导入批次、写入知识文档,并把批次自动挂到该改进项。</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="action-row">
|
||||
{action.linked_prompt_template_id && (
|
||||
<Link className="button button-soft" href="/admin/ai-prompts">
|
||||
查看 Prompt
|
||||
</Link>
|
||||
)}
|
||||
{action.linked_import_batch_id && (
|
||||
<span className="button button-soft button-static">
|
||||
<BookOpen size={16} />
|
||||
已关联知识批次
|
||||
</span>
|
||||
)}
|
||||
{action.status === "open" && (
|
||||
<button className="button button-soft" disabled={busy} onClick={() => void onUpdate(action, "in_progress")} type="button">
|
||||
<PlayCircle size={16} />
|
||||
@@ -658,7 +887,13 @@ function QualityActionItem({
|
||||
)}
|
||||
{["open", "in_progress"].includes(action.status) && (
|
||||
<>
|
||||
<button className="button button-primary" disabled={busy} onClick={() => void onUpdate(action, "resolved")} type="button">
|
||||
<button
|
||||
className="button button-primary"
|
||||
disabled={busy || Boolean(completionBlockReason)}
|
||||
onClick={() => void onUpdate(action, "resolved")}
|
||||
title={completionBlockReason || undefined}
|
||||
type="button"
|
||||
>
|
||||
<CheckCircle2 size={16} />
|
||||
标记完成
|
||||
</button>
|
||||
@@ -785,6 +1020,73 @@ function defaultResolutionNote(status: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function defaultKnowledgeDraft(action: QualityImprovementAction): KnowledgeDraft {
|
||||
const knowledgeBaseId = action.linked_knowledge_base_id || (action.dimension === "knowledge_base" ? action.key : "default");
|
||||
return {
|
||||
sourceName: `quality-action-${action.id}`,
|
||||
title: action.label || "质量改进补充知识",
|
||||
reference: action.root_cause_hint || action.source_label_codes.join("、") || action.id,
|
||||
content: [
|
||||
action.root_cause_hint || action.description || "请补充能支撑 AI 回答或审查结论的法规、制度、模板或案例依据。",
|
||||
"",
|
||||
`关联知识库:${knowledgeBaseId}`,
|
||||
`来源标签:${action.source_label_codes.join("、") || "无"}`
|
||||
].join("\n")
|
||||
};
|
||||
}
|
||||
|
||||
function getCompletionBlockReason(action: QualityImprovementAction) {
|
||||
if (action.action_type === "tune_prompt" && !(action.linked_prompt_template_id && action.linked_prompt_template_version)) {
|
||||
return "请先生成或关联 Prompt 草稿/版本,再标记完成。";
|
||||
}
|
||||
if (action.action_type === "improve_knowledge" && !(action.linked_knowledge_base_id && action.linked_import_batch_id)) {
|
||||
return "请先写入知识库导入批次,再标记完成。";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildPromptDraftContent(action: QualityImprovementAction, previousContent?: string) {
|
||||
const source = previousContent?.trim() || [
|
||||
"你是企业合同与法律问答助手。",
|
||||
"回答前先检索知识库,回答必须包含明确依据、适用关系、风险提示和需要补充的信息。",
|
||||
"如果没有检索到足够依据,必须说明当前依据不足,并建议人工复核。"
|
||||
].join("\n");
|
||||
|
||||
return [
|
||||
source,
|
||||
"",
|
||||
"# Quality improvement note",
|
||||
`- Action: ${action.id}`,
|
||||
`- Issue: ${action.title}`,
|
||||
`- Root cause: ${action.root_cause_hint || action.description || "待补充"}`,
|
||||
`- Labels: ${action.source_label_codes.join(", ") || "none"}`,
|
||||
"",
|
||||
"请在编辑后发布该草稿,并观察后续 AI Trace 的质量标签是否下降。"
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function readApiError(payload: unknown, status: number) {
|
||||
if (payload && typeof payload === "object") {
|
||||
const record = payload as { message?: unknown; detail?: unknown; payload?: unknown };
|
||||
if (typeof record.message === "string") {
|
||||
return record.message;
|
||||
}
|
||||
if (typeof record.detail === "string") {
|
||||
return record.detail;
|
||||
}
|
||||
if (record.payload && typeof record.payload === "object") {
|
||||
const nested = record.payload as { message?: unknown; detail?: unknown };
|
||||
if (typeof nested.detail === "string") {
|
||||
return nested.detail;
|
||||
}
|
||||
if (typeof nested.message === "string") {
|
||||
return nested.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
return `HTTP ${status}`;
|
||||
}
|
||||
|
||||
function attributionBusyKey(item: QualityAttributionItem) {
|
||||
return `create:${item.dimension}:${item.key}`;
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
const aiPlatformBaseUrl =
|
||||
process.env.AI_PLATFORM_API_BASE_URL ??
|
||||
process.env.NEXT_PUBLIC_AI_PLATFORM_API_BASE_URL ??
|
||||
"http://localhost:8101";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const targetUrl = new URL("/api/v1/knowledge-documents", aiPlatformBaseUrl);
|
||||
const body: unknown = await request.json().catch(() => null);
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
cache: "no-store"
|
||||
});
|
||||
const payload: unknown = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: "AI Platform knowledge document service returned an error.",
|
||||
status: response.status,
|
||||
payload
|
||||
},
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(payload);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: error instanceof Error ? error.message : "AI Platform knowledge document service is unavailable.",
|
||||
baseUrl: aiPlatformBaseUrl
|
||||
},
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
+34
@@ -36,3 +36,37 @@ export async function GET(request: Request) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const targetUrl = new URL("/api/v1/knowledge-import-batches", aiPlatformBaseUrl);
|
||||
const body: unknown = await request.json().catch(() => null);
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
cache: "no-store"
|
||||
});
|
||||
const payload: unknown = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: "AI Platform knowledge import batch service returned an error.",
|
||||
status: response.status,
|
||||
payload
|
||||
},
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(payload);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: error instanceof Error ? error.message : "AI Platform knowledge import batch service is unavailable.",
|
||||
baseUrl: aiPlatformBaseUrl
|
||||
},
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,6 +812,33 @@ select {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.quality-action-execution {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.quality-action-mini-form {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.quality-action-textarea {
|
||||
min-height: 110px;
|
||||
padding: 10px;
|
||||
resize: vertical;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.button-static {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.trend-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
Reference in New Issue
Block a user