fix: cite legal basis in chat answers
This commit is contained in:
+60
-1
@@ -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
|
||||
|
||||
@@ -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], "网络安全法第一条是什么")
|
||||
|
||||
Reference in New Issue
Block a user