forked from admin/PlantAI-rag-new
fix(r4-27): normalize_source title 空值回填(实体名/元数据标题字段,正文片段绝不参与)
消息级 sources items 的 title 为空导致前端文献列表渲染出正文片段。 回填优先级:顶层标题类字段(publication_title/article_title/journal_title/name) → metadata 标题类字段(paper_title/source_entity/name/source) → 顶层 source(断言主体实体名);全部缺失保持 null(前端有 fallback)。 paper_id/score/snippet 等既有字段不受影响。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,16 @@ def pick(obj: dict, *names: str, default=None):
|
||||
# ---------------------------------------------------------------------------
|
||||
# §8 Source 归一化(缺失字段键必须存在,值为 null)
|
||||
# ---------------------------------------------------------------------------
|
||||
# title 回填优先级(缺陷 #27,2026-09-11):title/paper_title 缺失时,
|
||||
# 依次从以下字段取可读文本(论文标题优先于实体名),全部缺失才保持 null。
|
||||
# 排除 content/evidence/text/snippet 正文字段——正文片段不是标题。
|
||||
_TITLE_FALLBACK_TOPLEVEL = (
|
||||
"publication_title", "article_title", "journal_title", "name",
|
||||
)
|
||||
_TITLE_FALLBACK_METADATA = (
|
||||
"paper_title", "publication_title", "article_title", "journal_title",
|
||||
"source_entity", "name", "source",
|
||||
)
|
||||
|
||||
def normalize_source(item: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""单个 source item → API.md §8 形态(所有键存在,缺失为 null)。
|
||||
@@ -41,6 +51,23 @@ def normalize_source(item: dict[str, Any] | None) -> dict[str, Any]:
|
||||
|
||||
source = pick(item, "source") # 断言主体实体名(上游语义)
|
||||
title = pick(item, "title", "paper_title")
|
||||
# 缺陷 #27:title 为空时按固定优先级回填——
|
||||
# 顶层标题类字段 → metadata 标题类字段 → 顶层 source(断言主体实体名)。
|
||||
# 正文字段绝不参与(正文片段不是标题)。
|
||||
if not title:
|
||||
for _key in _TITLE_FALLBACK_TOPLEVEL:
|
||||
_candidate = pick(item, _key)
|
||||
if _candidate:
|
||||
title = _candidate
|
||||
break
|
||||
if not title and isinstance(pick(item, "metadata"), dict):
|
||||
for _key in _TITLE_FALLBACK_METADATA:
|
||||
_candidate = pick(item["metadata"], _key)
|
||||
if _candidate:
|
||||
title = _candidate
|
||||
break
|
||||
if not title:
|
||||
title = source
|
||||
|
||||
doi = pick(item, "doi")
|
||||
url = pick(item, "url", "doi_url")
|
||||
|
||||
@@ -84,6 +84,52 @@ def test_normalize_source_only_doi_derives_url():
|
||||
assert src["metadata"] == {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# §8 title 回填(缺陷 #27,2026-09-11)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_normalize_source_title_fill_from_source_entity():
|
||||
"""title 为空 -> 回填 source(断言主体实体名),正文片段绝不参与。"""
|
||||
src = normalize_source({
|
||||
"source": "OsSPL14",
|
||||
"title": "",
|
||||
"evidence": "## 正文片段开头(不是标题)",
|
||||
"paper_id": "P1",
|
||||
})
|
||||
assert src["title"] == "OsSPL14"
|
||||
# title 即 source 回填:两者相等,source_entity 不落 metadata(不重复)
|
||||
assert src["metadata"].get("source_entity") is None
|
||||
assert src["paper_id"] == "P1"
|
||||
|
||||
|
||||
def test_normalize_source_title_fill_from_metadata():
|
||||
"""title/paper_title 均空 -> metadata 标题类字段回填。"""
|
||||
src = normalize_source({
|
||||
"source": "entity",
|
||||
"metadata": {"paper_title": "水稻抗病基因研究进展", "source_entity": "entity"},
|
||||
})
|
||||
assert src["title"] == "水稻抗病基因研究进展"
|
||||
|
||||
|
||||
def test_normalize_source_title_not_overridden():
|
||||
"""title 非空 -> 不回填(既有语义不变)。"""
|
||||
src = normalize_source({
|
||||
"title": "Real Title",
|
||||
"source": "Entity",
|
||||
"paper_title": "Other",
|
||||
"metadata": {"paper_title": "Meta"},
|
||||
})
|
||||
assert src["title"] == "Real Title"
|
||||
assert src["metadata"]["source_entity"] == "Entity"
|
||||
|
||||
|
||||
def test_normalize_source_title_all_empty_stays_none():
|
||||
"""全空 -> 保持 null(前端有 fallback)。"""
|
||||
src = normalize_source({"content": "snippet only", "paper_id": "P1"})
|
||||
assert src["title"] is None
|
||||
assert src["snippet"] == "snippet only"
|
||||
|
||||
|
||||
def test_normalize_source_alias_chains():
|
||||
src = normalize_source({
|
||||
"content": "snippet text",
|
||||
|
||||
Reference in New Issue
Block a user