fix: cite legal basis in chat answers

This commit is contained in:
2026-06-22 12:46:29 +08:00
parent 16f6bf9aac
commit ebb8ca0754
2 changed files with 68 additions and 1 deletions
+60 -1
View File
@@ -371,11 +371,17 @@ def _merge_local_sources_into_stream(upstream: Iterable[str], local_sources: Lis
return
emitted_sources = False
emitted_basis_intro = False
basis_intro = _build_legal_basis_intro(local_sources)
for chunk in upstream:
event = _parse_sse_event(chunk)
if event and event.get("type") == "text":
content = str(event.get("content") or "")
yield _format_sse_event({"type": "text", "content": _polish_chat_answer(content, message, local_sources)})
polished_content = _polish_chat_answer(content, message, local_sources)
if basis_intro and not emitted_basis_intro:
emitted_basis_intro = True
polished_content = _prepend_legal_basis_intro(polished_content, basis_intro)
yield _format_sse_event({"type": "text", "content": polished_content})
continue
if event and event.get("type") == "sources":
merged = _dedupe_sources([*(event.get("sources") or []), *local_sources])
@@ -388,6 +394,59 @@ def _merge_local_sources_into_stream(upstream: Iterable[str], local_sources: Lis
yield chunk
def _prepend_legal_basis_intro(content: str, basis_intro: str) -> str:
normalized_content = str(content or "").lstrip()
if not normalized_content:
return basis_intro
return f"{basis_intro}\n\n{normalized_content}"
def _build_legal_basis_intro(sources: List[Dict[str, Any]]) -> str:
legal_sources = [source for source in sources if _is_legal_article_source(source)]
if not legal_sources:
return ""
lines = [
"本次回答已先检索本地法规库,优先依据如下法条:",
]
for index, source in enumerate(legal_sources[:3], start=1):
citation = _format_legal_article_citation(source)
excerpt = _format_legal_article_excerpt(source.get("content"))
if excerpt:
lines.append(f"{index}. {citation}:{excerpt}")
else:
lines.append(f"{index}. {citation}")
lines.append("以下为结合上述依据形成的分析意见:")
return "\n".join(lines)
def _is_legal_article_source(source: Mapping[str, Any]) -> bool:
detail_path = str(source.get("detail_path") or "")
return source.get("source_type") == "legal_article" or detail_path.startswith("/legal-articles")
def _format_legal_article_citation(source: Mapping[str, Any]) -> str:
metadata = source.get("metadata") if isinstance(source.get("metadata"), Mapping) else {}
article_no = str(source.get("article_no") or "").strip()
source_title = str(metadata.get("source_title") or "").strip()
title = str(source.get("title") or "").strip()
if not source_title and article_no and title.endswith(article_no):
source_title = title[: -len(article_no)].strip()
if not source_title:
source_title = title
if source_title and article_no:
return f"《{source_title}》{article_no}"
return title or "本地法规库"
def _format_legal_article_excerpt(value: Any, max_length: int = 120) -> str:
text = " ".join(str(value or "").split())
if not text:
return ""
if len(text) <= max_length:
return text
return f"{text[:max_length].rstrip()}..."
def _polish_chat_answer(content: str, message: str, sources: List[Dict[str, Any]]) -> str:
if not _looks_like_raw_retrieval_answer(content):
return content
+8
View File
@@ -325,9 +325,13 @@ class ChatApiTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 200)
events = [line.removeprefix("data: ") for line in response.text.split("\n\n") if line.startswith("data: ")]
payloads = [json.loads(item) for item in events]
text_event = next(item for item in payloads if item["type"] == "text")
sources_event = next(item for item in payloads if item["type"] == "sources")
legal_source = next(item for item in sources_event["sources"] if item.get("source_type") == "legal_article")
self.assertIn("本次回答已先检索本地法规库", text_event["content"])
self.assertIn("优先依据如下法条", text_event["content"])
self.assertIn("以下为结合上述依据形成的分析意见", text_event["content"])
self.assertEqual(legal_source["source_id"], "lart-civil-585")
self.assertEqual(legal_source["detail_path"], "/legal-articles?article_id=lart-civil-585")
self.assertEqual(self.chat_service.stream_calls[0][3]["legalArticleRefs"][0]["id"], "lart-civil-585")
@@ -361,9 +365,13 @@ class ChatApiTestCase(unittest.TestCase):
self.assertEqual(response.status_code, 200)
events = [line.removeprefix("data: ") for line in response.text.split("\n\n") if line.startswith("data: ")]
payloads = [json.loads(item) for item in events]
text_event = next(item for item in payloads if item["type"] == "text")
sources_event = next(item for item in payloads if item["type"] == "sources")
legal_source = next(item for item in sources_event["sources"] if item.get("source_type") == "legal_article")
self.assertIn("本次回答已先检索本地法规库", text_event["content"])
self.assertIn("优先依据如下法条", text_event["content"])
self.assertIn("以下为结合上述依据形成的分析意见", text_event["content"])
self.assertEqual(legal_source["source_id"], "lart-cyber-001")
self.assertEqual(legal_source["detail_path"], "/legal-articles?article_id=lart-cyber-001")
self.assertEqual(legal_article_service.calls[0][1], "网络安全法第一条是什么")