Files
group_fqcd_jr/tools/knowledge_import_preflight.py
T

222 lines
8.9 KiB
Python
Raw Normal View History

2026-09-10 20:22:53 +08:00
"""将一期公开 QA 候选转换为不含外部写入的待审核导入清单。"""
import argparse
import json
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any
from app.core.knowledge_contracts import ALLOWED_KNOWLEDGE_COLLECTIONS
from app.service.knowledge_config import KnowledgeRuntimeConfig
# 仅允许一期三类公开知识按既定路由进入后续发布流程。
EXPECTED_COLLECTIONS = {
intent: collection
for intent, (collection, _top_k) in KnowledgeRuntimeConfig.DEFAULT_ROUTES.items()
}
# 公开候选必须具备的字段,缺失时不能生成不完整的发布清单。
REQUIRED_PUBLIC_FIELDS = frozenset({
"qa_id",
"title",
"question",
"paraphrases",
"answer",
"scope",
"intent",
"collection",
"execution_mode",
"retrieval_status",
"audience",
"agent_data_access",
"tags",
"source_type",
"source_file",
"source_version",
"review_status",
"status",
})
def _required_string(record: Mapping[str, object], field: str, qa_id: str) -> str:
"""读取非空字符串字段,拒绝将不完整资料带入后续发布阶段。"""
value = record.get(field)
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{qa_id}: {field} must be a non-empty string")
return value.strip()
def _required_strings(record: Mapping[str, object], field: str, qa_id: str) -> list[str]:
"""读取非空字符串数组,并保持来源中已经审核的条目顺序。"""
value = record.get(field)
if not isinstance(value, list) or not value:
raise ValueError(f"{qa_id}: {field} must be a non-empty string list")
strings = [item.strip() for item in value if isinstance(item, str) and item.strip()]
if len(strings) != len(value):
raise ValueError(f"{qa_id}: {field} must contain only non-empty strings")
return strings
def _is_rule_only(record: Mapping[str, object]) -> bool:
"""控制类记录必须由应用层固定路由处理,绝不能进入向量导入清单。"""
return (
record.get("collection") is None
and record.get("execution_mode") == "fixed_route"
and record.get("retrieval_status") == "rule_only"
)
def _validate_public_record(record: Mapping[str, object]) -> None:
"""验证候选是否符合一期公开知识与最小权限边界。"""
qa_id = _required_string(record, "qa_id", "<unknown>")
missing = sorted(field for field in REQUIRED_PUBLIC_FIELDS if field not in record)
if missing:
raise ValueError(f"{qa_id}: missing required fields: {', '.join(missing)}")
intent = _required_string(record, "intent", qa_id)
collection = _required_string(record, "collection", qa_id)
if intent not in EXPECTED_COLLECTIONS:
raise ValueError(f"{qa_id}: unsupported public intent: {intent}")
if collection not in ALLOWED_KNOWLEDGE_COLLECTIONS:
raise ValueError(f"{qa_id}: collection is not allowlisted: {collection}")
if collection != EXPECTED_COLLECTIONS[intent]:
raise ValueError(f"{qa_id}: collection does not match intent route")
if record.get("scope") != "public":
raise ValueError(f"{qa_id}: public vector record must have scope=public")
if record.get("execution_mode") != "vector_search":
raise ValueError(f"{qa_id}: public record must use vector_search")
if record.get("retrieval_status") != "approved_candidate":
raise ValueError(f"{qa_id}: record is not an approved candidate")
if record.get("review_status") != "approved_candidate":
raise ValueError(f"{qa_id}: review state cannot enter preflight")
if record.get("status") != "active":
raise ValueError(f"{qa_id}: inactive record cannot enter preflight")
if record.get("agent_data_access") != "none":
raise ValueError(f"{qa_id}: agent data access must remain none")
audience = _required_strings(record, "audience", qa_id)
if set(audience) != {"visitor", "authenticated_user"}:
raise ValueError(f"{qa_id}: audience must be visitor and authenticated_user")
_required_string(record, "title", qa_id)
_required_string(record, "question", qa_id)
_required_strings(record, "paraphrases", qa_id)
_required_string(record, "answer", qa_id)
_required_strings(record, "tags", qa_id)
_required_string(record, "source_type", qa_id)
_required_string(record, "source_file", qa_id)
_required_string(record, "source_version", qa_id)
def _public_entry(record: Mapping[str, object]) -> dict[str, object]:
"""构造供管理员审核的确定性条目,不生成数据库主键或向量。"""
qa_id = _required_string(record, "qa_id", "<unknown>")
question = _required_string(record, "question", qa_id)
paraphrases = _required_strings(record, "paraphrases", qa_id)
tags = _required_strings(record, "tags", qa_id)
answer = _required_string(record, "answer", qa_id)
content = {
"qa_id": qa_id,
"question": question,
"paraphrases": paraphrases,
"answer": answer,
"audience": _required_strings(record, "audience", qa_id),
"agent_data_access": "none",
"source_version": _required_string(record, "source_version", qa_id),
}
return {
"qa_id": qa_id,
"knowledge_type": _required_string(record, "intent", qa_id),
"title": _required_string(record, "title", qa_id),
"milvus_collection": _required_string(record, "collection", qa_id),
"version": _required_string(record, "source_version", qa_id),
"source_file": _required_string(record, "source_file", qa_id),
"source_type": _required_string(record, "source_type", qa_id),
"source_url": record.get("source_url"),
"effective_date": record.get("effective_date"),
"expire_date": record.get("expire_date"),
"tags": tags,
"content_text": json.dumps(content, ensure_ascii=False, separators=(",", ":")),
"retrieval_text": (
f"标准问题:{question}\n"
f"相似问法:{';'.join(paraphrases)}\n"
f"标签:{'、'.join(tags)}"
),
"snippet": question[:300],
# 管理员填入真实审核人并批准前,预检清单绝不伪装成已发布数据。
"review_status": "pending_review",
"status": "active",
}
def build_import_manifest(
records: Sequence[Mapping[str, object]], *, source_name: str
) -> dict[str, object]:
"""构建可复查的导入清单,并拒绝任何不满足公开边界的非控制类记录。"""
entries: list[dict[str, object]] = []
seen_ids: set[str] = set()
excluded_rule_records = 0
for record in records:
if _is_rule_only(record):
excluded_rule_records += 1
continue
_validate_public_record(record)
entry = _public_entry(record)
qa_id = str(entry["qa_id"])
if qa_id in seen_ids:
raise ValueError(f"duplicate qa_id: {qa_id}")
seen_ids.add(qa_id)
entries.append(entry)
return {
"source_name": source_name,
"summary": {
"total_records": len(records),
"eligible_records": len(entries),
"excluded_rule_records": excluded_rule_records,
"publication_state": "pending_review",
},
"records": entries,
}
def load_jsonl(source: Path) -> list[dict[str, object]]:
"""读取 UTF-8 JSONL,并为每个无效 JSON 行返回带行号的明确错误。"""
records: list[dict[str, object]] = []
for line_number, line in enumerate(source.read_text(encoding="utf-8").splitlines(), start=1):
if not line.strip():
continue
try:
value: Any = json.loads(line)
except json.JSONDecodeError as exc:
raise ValueError(f"line {line_number}: invalid JSON") from exc
if not isinstance(value, dict):
raise ValueError(f"line {line_number}: record must be an object")
records.append(value)
return records
def write_manifest(target: Path, manifest: Mapping[str, object]) -> None:
"""仅在调用者显式传入输出路径时,写入本地待审核 JSON 清单。"""
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
def main() -> int:
"""执行本地预检;该入口不读取配置也不连接任何外部服务。"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input", type=Path, required=True)
parser.add_argument("--output", type=Path)
arguments = parser.parse_args()
manifest = build_import_manifest(
load_jsonl(arguments.input), source_name=arguments.input.name
)
if arguments.output is None:
print(json.dumps(manifest, ensure_ascii=False, indent=2))
else:
write_manifest(arguments.output, manifest)
print(f"WROTE: {arguments.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())