Upload sources.py
Browse files- sources.py +98 -75
sources.py
CHANGED
|
@@ -117,81 +117,105 @@ def format_expandable_sources(source_nodes, question: str = "", answer: str = ""
|
|
| 117 |
Fail-closed behavior: if the full article cannot be resolved from ARTICLE_SOURCES or
|
| 118 |
SEMANTIC_UNITS, do not display node/evidence text as if it were the official source.
|
| 119 |
"""
|
| 120 |
-
seen = set()
|
| 121 |
-
blocks = []
|
| 122 |
-
for node_with_score in source_nodes:
|
| 123 |
-
meta = getattr(node_with_score.node, "metadata", {}) or {}
|
| 124 |
-
document_id = _metadata_document_id(meta)
|
| 125 |
-
article = meta.get("article", "") or meta.get("article_id", "")
|
| 126 |
-
title = meta.get("title", "") or meta.get("article_title", "")
|
| 127 |
-
key = _dedupe_key(document_id, article, title)
|
| 128 |
-
if key in seen:
|
| 129 |
-
continue
|
| 130 |
-
seen.add(key)
|
| 131 |
-
|
| 132 |
-
article_source = _lookup_article_source(document_id, article)
|
| 133 |
-
if article_source:
|
| 134 |
-
title = article_source.get("title") or title
|
| 135 |
-
body_text = article_source.get("source_text", "")
|
| 136 |
-
else:
|
| 137 |
-
unit = _lookup_semantic_unit(document_id, article)
|
| 138 |
-
title = unit.get("title") or title
|
| 139 |
-
body_text = unit.get("source_text", "")
|
| 140 |
-
|
| 141 |
-
if not body_text:
|
| 142 |
-
body_text = _missing_source_message(document_id, article)
|
| 143 |
-
|
| 144 |
-
label = _source_label(document_id, article, title, article_source.get("document_title", "") if article_source else "")
|
| 145 |
-
blocks.append(_render_source_block(label, body_text))
|
| 146 |
-
|
| 147 |
-
official = _official_source_link()
|
| 148 |
-
return "\n\n".join([_SOURCE_DISCLOSURE_STYLE] + blocks + [official])
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
def format_clause_sources(clauses: list[dict], question: str = "", answer: str = "") -> str:
|
| 152 |
-
"""Render full article sources for clause/evidence-based answers.
|
| 153 |
-
|
| 154 |
-
The answer engine selects small evidence spans for grounding, but the UI source panel
|
| 155 |
-
must show the full related article. Therefore this function never uses
|
| 156 |
-
`clause['source_text']` as a fallback. That field may contain only a clause, evidence
|
| 157 |
-
span, or generated/derived text. If the full article is missing, the UI displays an
|
| 158 |
-
explicit diagnostic message instead of pretending the evidence span is the source.
|
| 159 |
-
"""
|
| 160 |
-
blocks = []
|
| 161 |
-
seen_articles = set()
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
if
|
| 170 |
-
continue
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
if
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
return "\n\n".join([_SOURCE_DISCLOSURE_STYLE] + blocks + [official])
|
| 196 |
|
| 197 |
|
|
@@ -199,7 +223,6 @@ def get_article_source(document_id: str, article_id: str) -> dict[str, Any]:
|
|
| 199 |
"""Return the canonical full-article source already loaded by the runtime."""
|
| 200 |
return dict(_lookup_article_source(document_id, article_id) or {})
|
| 201 |
|
| 202 |
-
|
| 203 |
def _normalize_article_sources(article_sources: dict[str, dict]) -> tuple[dict[str, dict[str, Any]], set[str]]:
|
| 204 |
normalized: dict[str, dict[str, Any]] = {}
|
| 205 |
plain_key_documents: dict[str, set[str]] = {}
|
|
|
|
| 117 |
Fail-closed behavior: if the full article cannot be resolved from ARTICLE_SOURCES or
|
| 118 |
SEMANTIC_UNITS, do not display node/evidence text as if it were the official source.
|
| 119 |
"""
|
| 120 |
+
seen = set()
|
| 121 |
+
blocks = []
|
| 122 |
+
for node_with_score in source_nodes:
|
| 123 |
+
meta = getattr(node_with_score.node, "metadata", {}) or {}
|
| 124 |
+
document_id = _metadata_document_id(meta)
|
| 125 |
+
article = meta.get("article", "") or meta.get("article_id", "")
|
| 126 |
+
title = meta.get("title", "") or meta.get("article_title", "")
|
| 127 |
+
key = _dedupe_key(document_id, article, title)
|
| 128 |
+
if key in seen:
|
| 129 |
+
continue
|
| 130 |
+
seen.add(key)
|
| 131 |
+
|
| 132 |
+
article_source = _lookup_article_source(document_id, article)
|
| 133 |
+
if article_source:
|
| 134 |
+
title = article_source.get("title") or title
|
| 135 |
+
body_text = article_source.get("source_text", "")
|
| 136 |
+
else:
|
| 137 |
+
unit = _lookup_semantic_unit(document_id, article)
|
| 138 |
+
title = unit.get("title") or title
|
| 139 |
+
body_text = unit.get("source_text", "")
|
| 140 |
+
|
| 141 |
+
if not body_text:
|
| 142 |
+
body_text = _missing_source_message(document_id, article)
|
| 143 |
+
|
| 144 |
+
label = _source_label(document_id, article, title, article_source.get("document_title", "") if article_source else "")
|
| 145 |
+
blocks.append(_render_source_block(label, body_text))
|
| 146 |
+
|
| 147 |
+
official = _official_source_link()
|
| 148 |
+
return "\n\n".join([_SOURCE_DISCLOSURE_STYLE] + blocks + [official])
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _extract_text_article_sources(text: str) -> list[dict[str, str]]:
|
| 153 |
+
import re
|
| 154 |
+
sources = []
|
| 155 |
+
pattern = r"\b(2547|2914|2809)(?:\s+say[ıi]l[ıi]\s+kanun)?\s+(?:say[ıi]l[ıi]\s+)?(?:kanunun\s+)?(Ge[cç]ici\s+Madde\s+\d+|Ek\s+Madde\s+\d+|Madde\s+\d+(?:/[A-Za-zÇĞİÖŞÜçğıöşü])?)\b"
|
| 156 |
+
for match in re.finditer(pattern, text or "", flags=re.IGNORECASE):
|
| 157 |
+
code = match.group(1)
|
| 158 |
+
doc_id = f"TR-KANUN-{code}"
|
| 159 |
+
raw_art = match.group(2).strip()
|
| 160 |
+
art_norm = re.sub(r"\s+", " ", raw_art).title()
|
| 161 |
+
if art_norm.lower().startswith("madde "):
|
| 162 |
+
art_norm = f"Madde {art_norm[6:].upper()}"
|
| 163 |
+
elif art_norm.lower().startswith("ek madde "):
|
| 164 |
+
art_norm = f"Ek Madde {art_norm[9:]}"
|
| 165 |
+
elif art_norm.lower().startswith("geçici madde ") or art_norm.lower().startswith("gecici madde "):
|
| 166 |
+
art_norm = f"Geçici Madde {art_norm[13:].upper()}"
|
| 167 |
+
sources.append({"document_id": doc_id, "article_id": art_norm})
|
| 168 |
+
return sources
|
| 169 |
+
|
| 170 |
+
|
| 171 |
def format_clause_sources(clauses: list[dict], question: str = "", answer: str = "") -> str:
|
| 172 |
+
"""Render full article sources for clause/evidence-based answers.
|
| 173 |
+
|
| 174 |
+
The answer engine selects small evidence spans for grounding, but the UI source panel
|
| 175 |
+
must show the full related article. Therefore this function never uses
|
| 176 |
+
`clause['source_text']` as a fallback. That field may contain only a clause, evidence
|
| 177 |
+
span, or generated/derived text. If the full article is missing, the UI displays an
|
| 178 |
+
explicit diagnostic message instead of pretending the evidence span is the source.
|
| 179 |
+
"""
|
| 180 |
+
blocks = []
|
| 181 |
+
seen_articles = set()
|
| 182 |
+
all_clauses = list(clauses or [])
|
| 183 |
+
if answer:
|
| 184 |
+
all_clauses.extend(_extract_text_article_sources(answer))
|
| 185 |
+
|
| 186 |
+
for clause in all_clauses:
|
| 187 |
+
document_id = _clause_document_id(clause)
|
| 188 |
+
article = str(clause.get("article_id", "") or clause.get("article", "")).strip()
|
| 189 |
+
if not article:
|
| 190 |
+
continue
|
| 191 |
+
|
| 192 |
+
key = _dedupe_key(document_id, article)
|
| 193 |
+
if key in seen_articles:
|
| 194 |
+
continue
|
| 195 |
+
seen_articles.add(key)
|
| 196 |
+
|
| 197 |
+
article_source = _lookup_article_source(document_id, article)
|
| 198 |
+
title = ""
|
| 199 |
+
body_text = ""
|
| 200 |
+
if article_source:
|
| 201 |
+
title = str(article_source.get("title", "") or "").strip()
|
| 202 |
+
body_text = str(article_source.get("source_text", "") or "").strip()
|
| 203 |
+
|
| 204 |
+
if not body_text:
|
| 205 |
+
# Accept explicit full article text only. Do not fall back to clause['source_text'].
|
| 206 |
+
explicit_full_article = str(clause.get("article_source_text", "") or "").strip()
|
| 207 |
+
if explicit_full_article:
|
| 208 |
+
body_text = explicit_full_article
|
| 209 |
+
title = title or _source_title_from_clause(clause)
|
| 210 |
+
|
| 211 |
+
if not body_text:
|
| 212 |
+
body_text = _missing_source_message(document_id, article)
|
| 213 |
+
|
| 214 |
+
title = title or _source_title_from_clause(clause)
|
| 215 |
+
label = _source_label(document_id, article, title, article_source.get("document_title", "") if article_source else clause.get("document_title", ""))
|
| 216 |
+
blocks.append(_render_source_block(label, body_text))
|
| 217 |
+
|
| 218 |
+
official = _official_source_link()
|
| 219 |
return "\n\n".join([_SOURCE_DISCLOSURE_STYLE] + blocks + [official])
|
| 220 |
|
| 221 |
|
|
|
|
| 223 |
"""Return the canonical full-article source already loaded by the runtime."""
|
| 224 |
return dict(_lookup_article_source(document_id, article_id) or {})
|
| 225 |
|
|
|
|
| 226 |
def _normalize_article_sources(article_sources: dict[str, dict]) -> tuple[dict[str, dict[str, Any]], set[str]]:
|
| 227 |
normalized: dict[str, dict[str, Any]] = {}
|
| 228 |
plain_key_documents: dict[str, set[str]] = {}
|