feat: link ai quality actions to changes

This commit is contained in:
2026-06-23 10:22:48 +08:00
parent b2d60871a5
commit a756a0dd93
9 changed files with 450 additions and 8 deletions
@@ -0,0 +1,67 @@
# V2 AI 质量改进项关联真实变更记录 - 2026-06-23
## 背景
上一轮已经完成 Prompt 发布/回滚闭环。本轮继续推进“AI 质量问题归因 -> 改进动作 -> 真实配置变更 -> 效果回看”的闭环,避免质量改进项只停留在文字状态。
## 本轮完成内容
1. 改进项增加真实关联字段
- `linked_prompt_template_id`
- `linked_prompt_template_version`
- `linked_knowledge_base_id`
- `linked_import_batch_id`
2. AI Platform 自动推断关联对象
- Prompt 维度改进项会尝试关联对应 Prompt 模板和版本。
- 知识库维度改进项会关联知识库 ID 和该知识库最新导入批次。
- 创建接口仍支持前端或 SDK 显式传入关联字段。
3. 持久化兼容
- SQLAlchemy 模型增加对应字段。
- 自动兼容迁移会给已有 `ai_quality_improvement_actions` 表补列。
- 内存仓库与 SQL 仓库均支持创建、更新、列表、看板返回关联字段。
4. OpenAPI 与 SDK 同步
- `AiQualityImprovementActionCreate``AiQualityImprovementActionUpdate``AiQualityImprovementAction` 同步新增关联字段。
- Python SDK 支持创建和更新改进项时传入关联字段。
5. 管理后台质量运营页增强
- 页面加载 Prompt 模板和知识库导入批次候选。
- 从 Prompt 归因生成改进项时,自动关联 Prompt 模板 ID 和版本。
- 从知识库归因生成改进项时,自动关联知识库 ID 和最新导入批次。
- 改进项卡片展示“关联变更”,并提供跳转 Prompt 中心入口。
- 新增 `/api/ai-platform/knowledge-import-batches` 前端代理。
## 验证结果
- `python -m pytest yuqei-ai-platform/services/ai-platform-api/tests -q`
- 26 passed
- `python -m pytest yuqei-ai-sdk-python/tests -q`
- 2 passed
- `npm run web:typecheck`
- passed
- `npm run web:build`
- passed
- OpenAPI YAML 解析
- passed
## 当前闭环价值
现在质量运营路径变成:
1. 质量看板发现某个模型、Prompt、知识库或标签异常集中。
2. 管理员从归因卡片生成改进项。
3. 改进项自动挂上相关 Prompt 版本或知识库导入批次。
4. 管理员去 Prompt 中心发布新版本,或补充知识库导入批次。
5. 后续 AI 调用继续进入质量看板,改进项显示处理后异常率、质量分、待处理标签变化。
这让“调整 Prompt”“补知识库”开始具备可追溯的配置变更记录。
## 下一步建议
下一步可以进入“改进动作执行页”:
-`tune_prompt` 改进项,提供直接创建 Prompt 草稿的入口。
-`improve_knowledge` 改进项,提供上传/导入知识文档入口。
- 改进项完成时自动要求填写“实际变更对象”,防止误标完成。
@@ -1666,6 +1666,22 @@ components:
root_cause_hint:
type: string
nullable: true
linked_prompt_template_id:
type: string
nullable: true
maxLength: 64
linked_prompt_template_version:
type: string
nullable: true
maxLength: 64
linked_knowledge_base_id:
type: string
nullable: true
maxLength: 64
linked_import_batch_id:
type: string
nullable: true
maxLength: 64
evaluation_days:
type: integer
minimum: 1
@@ -1690,6 +1706,22 @@ components:
resolution_note:
type: string
nullable: true
linked_prompt_template_id:
type: string
nullable: true
maxLength: 64
linked_prompt_template_version:
type: string
nullable: true
maxLength: 64
linked_knowledge_base_id:
type: string
nullable: true
maxLength: 64
linked_import_batch_id:
type: string
nullable: true
maxLength: 64
AiQualityImprovementAction:
allOf:
- $ref: "#/components/schemas/AiQualityImprovementActionCreate"
@@ -172,6 +172,10 @@ class AiQualityImprovementActionModel(Base):
source_label_codes_json: Mapped[str] = mapped_column(Text, default="[]")
source_run_ids_json: Mapped[str] = mapped_column(Text, default="[]")
root_cause_hint: Mapped[str | None] = mapped_column(Text, nullable=True)
linked_prompt_template_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
linked_prompt_template_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
linked_knowledge_base_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
linked_import_batch_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
evaluation_days: Mapped[int] = mapped_column(Integer, default=14)
baseline_total_runs: Mapped[int] = mapped_column(Integer, default=0)
baseline_affected_runs: Mapped[int] = mapped_column(Integer, default=0)
@@ -335,6 +335,10 @@ class AiQualityImprovementActionCreate(BaseModel):
source_label_codes: list[str] = Field(default_factory=list)
source_run_ids: list[str] = Field(default_factory=list)
root_cause_hint: str | None = None
linked_prompt_template_id: str | None = Field(default=None, max_length=64)
linked_prompt_template_version: str | None = Field(default=None, max_length=64)
linked_knowledge_base_id: str | None = Field(default=None, max_length=64)
linked_import_batch_id: str | None = Field(default=None, max_length=64)
evaluation_days: int = Field(default=14, ge=1, le=90)
@@ -343,6 +347,10 @@ class AiQualityImprovementActionUpdate(BaseModel):
owner: str | None = Field(default=None, max_length=128)
priority: str | None = Field(default=None, pattern="^(low|medium|high|critical)$")
resolution_note: str | None = None
linked_prompt_template_id: str | None = Field(default=None, max_length=64)
linked_prompt_template_version: str | None = Field(default=None, max_length=64)
linked_knowledge_base_id: str | None = Field(default=None, max_length=64)
linked_import_batch_id: str | None = Field(default=None, max_length=64)
class AiQualityImprovementAction(AiQualityImprovementActionCreate):
@@ -866,6 +874,11 @@ class InMemoryAiPlatformRepository:
now = datetime.now(UTC)
with self._lock:
baseline = _calculate_improvement_action_stats(payload, self.ai_run_audits)
payload = _with_inferred_action_links(
payload,
prompt_templates=self.prompt_templates.values(),
knowledge_import_batches=self.knowledge_import_batches.values(),
)
action = AiQualityImprovementAction(
id=f"qa-action-{uuid4().hex[:12]}",
title=payload.title or _default_improvement_action_title(payload),
@@ -906,6 +919,14 @@ class InMemoryAiPlatformRepository:
update["priority"] = payload.priority
if payload.resolution_note is not None:
update["resolution_note"] = payload.resolution_note
if payload.linked_prompt_template_id is not None:
update["linked_prompt_template_id"] = payload.linked_prompt_template_id
if payload.linked_prompt_template_version is not None:
update["linked_prompt_template_version"] = payload.linked_prompt_template_version
if payload.linked_knowledge_base_id is not None:
update["linked_knowledge_base_id"] = payload.linked_knowledge_base_id
if payload.linked_import_batch_id is not None:
update["linked_import_batch_id"] = payload.linked_import_batch_id
updated = action.model_copy(update=update)
self.quality_improvement_actions[action_id] = updated
return _materialize_improvement_action(updated, self.ai_run_audits)
@@ -956,15 +977,15 @@ class SqlAlchemyAiPlatformRepository:
self._vector_search_backend = vector_search_backend
if auto_create:
Base.metadata.create_all(self._engine)
self._ensure_prompt_template_columns()
self._ensure_compat_columns()
def _ensure_prompt_template_columns(self) -> None:
def _ensure_compat_columns(self) -> None:
with self._engine.begin() as connection:
dialect = self._engine.dialect.name
existing_columns: set[str]
prompt_columns: set[str]
if dialect == "sqlite":
rows = connection.execute(text("PRAGMA table_info(prompt_templates)")).mappings().all()
existing_columns = {str(row["name"]) for row in rows}
prompt_columns = {str(row["name"]) for row in rows}
else:
rows = connection.execute(
text(
@@ -972,15 +993,38 @@ class SqlAlchemyAiPlatformRepository:
"where table_name = 'prompt_templates'"
)
).mappings().all()
existing_columns = {str(row["column_name"]) for row in rows}
if "status" not in existing_columns:
prompt_columns = {str(row["column_name"]) for row in rows}
if "status" not in prompt_columns:
connection.execute(text("ALTER TABLE prompt_templates ADD COLUMN status VARCHAR(32) DEFAULT 'draft'"))
if "rollback_from_version" not in existing_columns:
if "rollback_from_version" not in prompt_columns:
connection.execute(text("ALTER TABLE prompt_templates ADD COLUMN rollback_from_version VARCHAR(32)"))
if "published_at" not in existing_columns:
if "published_at" not in prompt_columns:
column_type = "TIMESTAMP" if dialect == "sqlite" else "TIMESTAMP WITH TIME ZONE"
connection.execute(text(f"ALTER TABLE prompt_templates ADD COLUMN published_at {column_type}"))
if dialect == "sqlite":
rows = connection.execute(text("PRAGMA table_info(ai_quality_improvement_actions)")).mappings().all()
action_columns = {str(row["name"]) for row in rows}
else:
rows = connection.execute(
text(
"select column_name from information_schema.columns "
"where table_name = 'ai_quality_improvement_actions'"
)
).mappings().all()
action_columns = {str(row["column_name"]) for row in rows}
action_column_specs = {
"linked_prompt_template_id": "VARCHAR(64)",
"linked_prompt_template_version": "VARCHAR(64)",
"linked_knowledge_base_id": "VARCHAR(64)",
"linked_import_batch_id": "VARCHAR(64)",
}
for column_name, column_type in action_column_specs.items():
if column_name not in action_columns:
connection.execute(
text(f"ALTER TABLE ai_quality_improvement_actions ADD COLUMN {column_name} {column_type}")
)
def upsert_provider_config(self, payload: ProviderConfigCreate) -> ProviderConfig:
now = datetime.now(UTC)
with self._session_factory.begin() as session:
@@ -1589,6 +1633,17 @@ class SqlAlchemyAiPlatformRepository:
).all()
runs = [_ai_run_audit_from_model(row) for row in audit_rows]
baseline = _calculate_improvement_action_stats(payload, runs)
payload = _with_inferred_action_links(
payload,
prompt_templates=[
_prompt_template_from_model(row)
for row in session.scalars(select(PromptTemplateModel)).all()
],
knowledge_import_batches=[
_knowledge_import_batch_from_model(row)
for row in session.scalars(select(KnowledgeImportBatchModel)).all()
],
)
row = AiQualityImprovementActionModel(
id=f"qa-action-{uuid4().hex[:12]}",
dimension=payload.dimension,
@@ -1603,6 +1658,10 @@ class SqlAlchemyAiPlatformRepository:
source_label_codes_json=_encode_json_list(payload.source_label_codes),
source_run_ids_json=_encode_json_list(payload.source_run_ids),
root_cause_hint=payload.root_cause_hint,
linked_prompt_template_id=payload.linked_prompt_template_id,
linked_prompt_template_version=payload.linked_prompt_template_version,
linked_knowledge_base_id=payload.linked_knowledge_base_id,
linked_import_batch_id=payload.linked_import_batch_id,
evaluation_days=payload.evaluation_days,
baseline_total_runs=baseline["total_runs"],
baseline_affected_runs=baseline["affected_runs"],
@@ -1632,6 +1691,14 @@ class SqlAlchemyAiPlatformRepository:
row.priority = payload.priority
if payload.resolution_note is not None:
row.resolution_note = payload.resolution_note
if payload.linked_prompt_template_id is not None:
row.linked_prompt_template_id = payload.linked_prompt_template_id
if payload.linked_prompt_template_version is not None:
row.linked_prompt_template_version = payload.linked_prompt_template_version
if payload.linked_knowledge_base_id is not None:
row.linked_knowledge_base_id = payload.linked_knowledge_base_id
if payload.linked_import_batch_id is not None:
row.linked_import_batch_id = payload.linked_import_batch_id
row.updated_at = now
row.resolved_at = now if payload.status in {"resolved", "false_positive", "closed"} else None
@@ -1926,6 +1993,10 @@ def _quality_improvement_action_from_model(row: AiQualityImprovementActionModel)
source_label_codes=[str(item) for item in _decode_json_list(row.source_label_codes_json)],
source_run_ids=[str(item) for item in _decode_json_list(row.source_run_ids_json)],
root_cause_hint=row.root_cause_hint,
linked_prompt_template_id=row.linked_prompt_template_id,
linked_prompt_template_version=row.linked_prompt_template_version,
linked_knowledge_base_id=row.linked_knowledge_base_id,
linked_import_batch_id=row.linked_import_batch_id,
evaluation_days=row.evaluation_days,
baseline_total_runs=row.baseline_total_runs,
baseline_affected_runs=row.baseline_affected_runs,
@@ -1938,6 +2009,61 @@ def _quality_improvement_action_from_model(row: AiQualityImprovementActionModel)
)
def _with_inferred_action_links(
payload: AiQualityImprovementActionCreate,
*,
prompt_templates: Iterable[PromptTemplate],
knowledge_import_batches: Iterable[KnowledgeImportBatch],
) -> AiQualityImprovementActionCreate:
update: dict[str, str] = {}
if payload.dimension == "prompt_template" and not payload.linked_prompt_template_id:
prompt = _find_prompt_template_link(payload.key, prompt_templates)
if prompt is not None:
update["linked_prompt_template_id"] = prompt.id
update["linked_prompt_template_version"] = prompt.version
if payload.dimension == "knowledge_base" or payload.linked_knowledge_base_id:
if payload.dimension == "knowledge_base" and not payload.linked_knowledge_base_id and payload.key != "no_knowledge_source":
update["linked_knowledge_base_id"] = payload.key
knowledge_base_id = payload.linked_knowledge_base_id or update.get("linked_knowledge_base_id")
if knowledge_base_id and not payload.linked_import_batch_id:
batch = _find_latest_import_batch_link(knowledge_base_id, knowledge_import_batches)
if batch is not None:
update["linked_import_batch_id"] = batch.id
return payload.model_copy(update=update) if update else payload
def _find_prompt_template_link(
key: str,
prompt_templates: Iterable[PromptTemplate],
) -> PromptTemplate | None:
candidates = [
template
for template in prompt_templates
if template.id == key or template.name == key or f"{template.name}:{template.version}" == key
]
if not candidates:
return None
return sorted(
candidates,
key=lambda item: item.published_at or item.updated_at,
reverse=True,
)[0]
def _find_latest_import_batch_link(
knowledge_base_id: str,
batches: Iterable[KnowledgeImportBatch],
) -> KnowledgeImportBatch | None:
candidates = [
batch
for batch in batches
if batch.knowledge_base_id == knowledge_base_id
]
if not candidates:
return None
return sorted(candidates, key=lambda item: item.updated_at, reverse=True)[0]
def _ensure_utc(value: datetime) -> datetime:
if value.tzinfo is None:
return value.replace(tzinfo=UTC)
@@ -326,6 +326,53 @@ def test_quality_attribution_can_create_and_resolve_improvement_action() -> None
assert dashboard_action["effect_summary"]
def test_quality_improvement_action_links_prompt_and_knowledge_changes() -> None:
repository = InMemoryAiPlatformRepository()
repository.seed_defaults()
prompt = repository.get_prompt_template("legal_qa.default")
assert prompt is not None
batch = repository.create_knowledge_import_batch(
KnowledgeImportBatchCreate(
knowledge_base_id="laws-cn",
source_name="civil-code-refresh",
source_type="law_import",
)
)
prompt_action = repository.create_quality_improvement_action(
AiQualityImprovementActionCreate(
dimension="prompt_template",
key=prompt.id,
label="legal_qa.default",
action_type="tune_prompt",
evaluation_days=7,
)
)
assert prompt_action.linked_prompt_template_id == prompt.id
assert prompt_action.linked_prompt_template_version == prompt.version
knowledge_action = repository.create_quality_improvement_action(
AiQualityImprovementActionCreate(
dimension="knowledge_base",
key="laws-cn",
label="laws-cn",
action_type="improve_knowledge",
evaluation_days=7,
)
)
assert knowledge_action.linked_knowledge_base_id == "laws-cn"
assert knowledge_action.linked_import_batch_id == batch.id
updated = repository.update_quality_improvement_action(
knowledge_action.id,
AiQualityImprovementActionUpdate(
status="in_progress",
linked_import_batch_id="kb-batch-manual",
),
)
assert updated.linked_import_batch_id == "kb-batch-manual"
def test_knowledge_document_can_be_added_and_searched() -> None:
client = make_client()
@@ -345,6 +345,10 @@ class AiQualityImprovementActionCreate(BaseModel):
source_label_codes: list[str] = Field(default_factory=list)
source_run_ids: list[str] = Field(default_factory=list)
root_cause_hint: str | None = None
linked_prompt_template_id: str | None = None
linked_prompt_template_version: str | None = None
linked_knowledge_base_id: str | None = None
linked_import_batch_id: str | None = None
evaluation_days: int = 14
@@ -353,6 +357,10 @@ class AiQualityImprovementActionUpdate(BaseModel):
owner: str | None = Field(default=None, max_length=128)
priority: str | None = None
resolution_note: str | None = None
linked_prompt_template_id: str | None = None
linked_prompt_template_version: str | None = None
linked_knowledge_base_id: str | None = None
linked_import_batch_id: str | None = None
class AiQualityImprovementAction(AiQualityImprovementActionCreate):
@@ -115,6 +115,10 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
"source_label_codes": ["source_limited"],
"source_run_ids": ["run-1"],
"root_cause_hint": "Model quality issues are concentrated.",
"linked_prompt_template_id": "prompt-1",
"linked_prompt_template_version": "v2",
"linked_knowledge_base_id": "laws-cn",
"linked_import_batch_id": "kb-batch-1",
"evaluation_days": 7,
"baseline_total_runs": 1,
"baseline_affected_runs": 1,
@@ -621,6 +625,7 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
assert payload["dimension"] == "model"
assert payload["action_type"] == "tune_model"
assert payload["source_label_codes"] == ["source_limited"]
assert payload["linked_prompt_template_id"] == "prompt-1"
return httpx.Response(200, json=quality_action_payload())
if request.url.path == "/api/v1/audit/quality-improvement-actions":
query = parse_qs(request.url.query.decode())
@@ -636,6 +641,7 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
assert request.method == "PATCH"
assert payload["status"] == "resolved"
assert payload["resolution_note"] == "Provider config was tuned."
assert payload["linked_import_batch_id"] == "kb-batch-2"
return httpx.Response(
200,
json=quality_action_payload(
@@ -802,10 +808,13 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
priority="high",
source_label_codes=["source_limited"],
source_run_ids=["run-1"],
linked_prompt_template_id="prompt-1",
evaluation_days=7,
)
)
assert created_action.status == "open"
assert created_action.linked_prompt_template_id == "prompt-1"
assert created_action.linked_import_batch_id == "kb-batch-1"
assert created_action.baseline_affected_runs == 1
assert created_action.effect_status == "unchanged"
listed_actions = client.list_quality_improvement_actions(
@@ -823,6 +832,7 @@ def test_ai_platform_client_covers_admin_data_endpoints() -> None:
AiQualityImprovementActionUpdate(
status="resolved",
resolution_note="Provider config was tuned.",
linked_import_batch_id="kb-batch-2",
),
)
assert updated_action.status == "resolved"
@@ -84,6 +84,10 @@ type QualityImprovementAction = {
source_label_codes: string[];
source_run_ids: string[];
root_cause_hint?: string | null;
linked_prompt_template_id?: string | null;
linked_prompt_template_version?: string | null;
linked_knowledge_base_id?: string | null;
linked_import_batch_id?: string | null;
evaluation_days: number;
baseline_total_runs: number;
baseline_affected_runs: number;
@@ -112,6 +116,24 @@ type QualityDashboard = {
improvement_actions?: QualityImprovementAction[];
};
type PromptTemplateOption = {
id: string;
name: string;
version: string;
status: string;
published_at?: string | null;
updated_at: string;
};
type KnowledgeImportBatchOption = {
id: string;
knowledge_base_id: string;
source_name: string;
source_type: string;
status: string;
updated_at: string;
};
type LoadState =
| { status: "loading" }
| { status: "ready"; dashboard: QualityDashboard }
@@ -181,6 +203,8 @@ export default function AiQualityDashboardPage() {
const [days, setDays] = useState(14);
const [actionMessage, setActionMessage] = useState<string | null>(null);
const [actionBusyKey, setActionBusyKey] = useState<string | null>(null);
const [promptOptions, setPromptOptions] = useState<PromptTemplateOption[]>([]);
const [importBatchOptions, setImportBatchOptions] = useState<KnowledgeImportBatchOption[]>([]);
const [filters, setFilters] = useState<ActionFilters>({
status: "",
owner: "",
@@ -209,8 +233,24 @@ export default function AiQualityDashboardPage() {
}
}
async function loadLinkOptions() {
const [promptResponse, batchResponse] = await Promise.allSettled([
fetch("/api/ai-platform/prompt-templates", { cache: "no-store" }),
fetch("/api/ai-platform/knowledge-import-batches?limit=50", { cache: "no-store" })
]);
if (promptResponse.status === "fulfilled" && promptResponse.value.ok) {
const payload = await promptResponse.value.json().catch(() => []);
setPromptOptions(Array.isArray(payload) ? payload : []);
}
if (batchResponse.status === "fulfilled" && batchResponse.value.ok) {
const payload = await batchResponse.value.json().catch(() => []);
setImportBatchOptions(Array.isArray(payload) ? payload : []);
}
}
useEffect(() => {
void loadDashboard(days);
void loadLinkOptions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -249,6 +289,7 @@ export default function AiQualityDashboardPage() {
priority: inferPriority(item),
source_label_codes: item.top_label_codes,
root_cause_hint: item.root_cause_hint,
...inferActionLinkPayload(item, promptOptions, importBatchOptions),
evaluation_days: days
})
});
@@ -257,6 +298,7 @@ export default function AiQualityDashboardPage() {
throw new Error(payload?.message ?? `HTTP ${response.status}`);
}
setActionMessage("已生成改进项,可以在下方队列继续处理。");
await loadLinkOptions();
await loadDashboard(days);
} catch (error) {
setActionMessage(error instanceof Error ? error.message : "生成改进项失败");
@@ -600,8 +642,14 @@ function QualityActionItem({
<span> {formatSignedPercent(action.affected_rate_delta)}</span>
</div>
{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>}
<div className="action-row">
{action.linked_prompt_template_id && (
<Link className="button button-soft" href="/admin/ai-prompts">
Prompt
</Link>
)}
{action.status === "open" && (
<button className="button button-soft" disabled={busy} onClick={() => void onUpdate(action, "in_progress")} type="button">
<PlayCircle size={16} />
@@ -625,6 +673,20 @@ function QualityActionItem({
);
}
function LinkedChangeSummary({ action }: { action: QualityImprovementAction }) {
const links = [
action.linked_prompt_template_id
? `Prompt ${action.linked_prompt_template_version || action.linked_prompt_template_id}`
: null,
action.linked_knowledge_base_id ? `知识库 ${action.linked_knowledge_base_id}` : null,
action.linked_import_batch_id ? `导入批次 ${action.linked_import_batch_id}` : null
].filter(Boolean);
if (links.length === 0) {
return null;
}
return <span className="item-meta">{links.join(" · ")}</span>;
}
function filterImprovementActions(actions: QualityImprovementAction[], filters: ActionFilters) {
return actions.filter((action) => (
(!filters.status || action.status === filters.status)
@@ -665,6 +727,54 @@ function inferPriority(item: QualityAttributionItem) {
return "medium";
}
function inferActionLinkPayload(
item: QualityAttributionItem,
promptOptions: PromptTemplateOption[],
importBatchOptions: KnowledgeImportBatchOption[]
) {
if (item.dimension === "prompt_template") {
const prompt = findPromptOption(item.key, promptOptions);
return {
linked_prompt_template_id: prompt?.id ?? item.key,
linked_prompt_template_version: prompt?.version
};
}
if (item.dimension === "knowledge_base" && item.key !== "no_knowledge_source") {
const batch = findLatestImportBatchOption(item.key, importBatchOptions);
return {
linked_knowledge_base_id: item.key,
linked_import_batch_id: batch?.id
};
}
if (item.dimension === "label_code" && item.key.includes("source")) {
const batch = findLatestImportBatchOption("default", importBatchOptions);
return {
linked_knowledge_base_id: "default",
linked_import_batch_id: batch?.id
};
}
return {};
}
function findPromptOption(key: string, options: PromptTemplateOption[]) {
const matches = options.filter((option) => (
option.id === key
|| option.name === key
|| `${option.name}:${option.version}` === key
));
return matches.sort((left, right) => {
const leftTime = new Date(left.published_at || left.updated_at).getTime();
const rightTime = new Date(right.published_at || right.updated_at).getTime();
return rightTime - leftTime;
})[0];
}
function findLatestImportBatchOption(knowledgeBaseId: string, options: KnowledgeImportBatchOption[]) {
return options
.filter((option) => option.knowledge_base_id === knowledgeBaseId)
.sort((left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime())[0];
}
function defaultResolutionNote(status: string) {
if (status === "resolved") {
return "已处理,等待后续 AI 调用数据验证异常率是否下降。";
@@ -0,0 +1,38 @@
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 GET(request: Request) {
const requestUrl = new URL(request.url);
const targetUrl = new URL("/api/v1/knowledge-import-batches", aiPlatformBaseUrl);
requestUrl.searchParams.forEach((value, key) => {
targetUrl.searchParams.set(key, value);
});
try {
const response = await fetch(targetUrl, { 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 }
);
}
}