Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions apps/api/tests/contract/test_evidence_renderer_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from shared.services.retrieval.execution.routes import _render_rows_evidence


def test_render_rows_evidence_should_group_by_traceable_path() -> None:
rows = [
{
"chunk_id": "c2",
"content": "second section content",
"sort_order": 2,
"source": {
"source_file_name": "alpha.pdf",
"section_path": "Alpha / Two",
},
},
{
"chunk_id": "c1",
"content": "first section content\nwith more detail",
"sort_order": 1,
"source": {
"source_file_name": "alpha.pdf",
"section_path": "Alpha / One",
},
},
{
"chunk_id": "c3",
"content": "<table><tr><td>metric</td></tr></table>",
"source_file_name": "beta.pdf",
"section_path": "Beta / Table",
},
]

evidence_text = _render_rows_evidence(rows)

assert "[E1]" in evidence_text
assert "[E2]" in evidence_text
assert "[E3]" in evidence_text
assert "[§ alpha.pdf / Alpha / One]" in evidence_text
assert "[§ alpha.pdf / Alpha / Two]" in evidence_text
assert "[§ beta.pdf / Beta / Table]" in evidence_text
assert "first section content" in evidence_text
assert "second section content" in evidence_text
assert "<table><tr><td>metric</td></tr></table>" in evidence_text
assert "[Document]" not in evidence_text
assert "▸" not in evidence_text
assert "┈" not in evidence_text
43 changes: 0 additions & 43 deletions apps/api/tests/contract/test_legacy_evidence_renderer_contract.py

This file was deleted.

4 changes: 2 additions & 2 deletions apps/api/tests/contract/test_retrieval_lazy_tree_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def node_meta(self, section_id: str) -> NodeMeta:
budget_chars=100,
)

assert result.evidence_text == "[E1]\n[§ Child]\nevidence"
assert result.evidence_text == "[E1]\n[§ Root / Child]\nevidence"
assert provider.metadata_calls == 0


Expand Down Expand Up @@ -102,4 +102,4 @@ def test_evidence_pack_identifies_header_owners_from_parent_chain() -> None:
)

assert result.kept_chunks == [chunks[1]]
assert result.evidence_text == "[E1]\n[§ Child]\nchild evidence"
assert result.evidence_text == "[E1]\n[§ Root / Parent / Child]\nchild evidence"
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ async def test_table_result_assembly_uses_summary_not_html() -> None:
assert "企业名称;统一社会信用代码" in content
assert "SHOULD NOT LEAK" not in content
assert "<table" not in content
assert "[tables/" not in content
assert content.index("见表") < content.index("[Table:")


@pytest.mark.asyncio
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
from shared.services.retrieval.hydration.result_assembly import (
assemble_retrieval_results,
)
from shared.services.retrieval.hydration.legacy_evidence import (
render_legacy_evidence_text,
)
from shared.services.retrieval.hydration.evidence_text import render_evidence_blocks
from shared.services.retrieval.execution.route_types import (
RetrievalRouteContext,
RetrievalRouteOutcome,
Expand All @@ -40,6 +38,28 @@ def open_fresh_database_context() -> AbstractAsyncContextManager[AsyncSession]:
return get_db_context()


def _evidence_path_header(row: dict) -> str:
source = row.get("source")
if not isinstance(source, dict):
source = row
file_name = str(source.get("source_file_name") or "").strip()
section_path = str(source.get("section_path") or "").strip()
if file_name and section_path:
return f"{file_name} / {section_path}"
return file_name or section_path


def _render_rows_evidence(rows: list[dict]) -> str:
groups: dict[str, list[str]] = {}
for row in rows:
header = _evidence_path_header(row)
content = str(row.get("content") or "").strip()
if not content:
continue
groups.setdefault(header, []).append(content)
return render_evidence_blocks(list(groups.items()))


async def run_retrieval_route(
context: RetrievalRouteContext,
) -> RetrievalRouteOutcome:
Expand Down Expand Up @@ -102,7 +122,7 @@ async def _try_run_small_corpus_route(
"namespace": context.namespace,
"query": context.query,
"router_used": "small_corpus_all",
"evidence_text": render_legacy_evidence_text(results),
"evidence_text": _render_rows_evidence(results),
"answer_text": "",
"results": results,
}
Expand Down Expand Up @@ -157,7 +177,7 @@ async def _run_classic_topk_route(
"namespace": context.namespace,
"query": context.query,
"router_used": "classic_topk",
"evidence_text": render_legacy_evidence_text(results),
"evidence_text": _render_rows_evidence(results),
"answer_text": "",
"results": results,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Insert connected image/table bodies at text placeholders.

Replaces ``[images/...]`` / ``[tables/...]`` (or ``connect_to.ref``) with the
asset display body, with a newline before and after. Targets not found at a
placeholder are appended once. Leftover path placeholders are stripped.
"""

from __future__ import annotations

import re
from collections.abc import Mapping, Sequence
from typing import Any

_PATH_REF_RE = re.compile(r"\[(?:images|tables)/[^\]\n]+\]")
_SAME_AS_RE = re.compile(r"\[SAME-AS [^\]]+\]")


def strip_path_placeholders(content: str) -> str:
text = _PATH_REF_RE.sub("", content)
text = _SAME_AS_RE.sub("", text)
return text.strip()


def inline_assets_at_placeholders(
host_text: str,
*,
connections: Sequence[Mapping[str, Any]] | Sequence[Any],
display_by_target: Mapping[str, str],
) -> tuple[str, set[str]]:
"""Return (body, embedded_target_ids).

``display_by_target`` maps chunk_id → display body. Only targets present in
that map are inserted. Each target is inserted at most once.
"""
text = str(host_text or "")
embedded: set[str] = set()
pending_append: list[tuple[str, str]] = []

for item in connections or ():
if not isinstance(item, Mapping):
continue
target_id = str(item.get("target") or "").strip()
if not target_id or target_id in embedded:
continue
body = str(display_by_target.get(target_id) or "").strip()
if not body:
continue

ref = str(item.get("ref") or "").strip()
placed = False
for candidate in _ref_candidates(ref):
if candidate and candidate in text:
text = text.replace(candidate, f"\n{body}\n", 1)
embedded.add(target_id)
placed = True
break
if not placed:
pending_append.append((target_id, body))

for target_id, body in pending_append:
if target_id in embedded:
continue
if text.strip():
text = f"{text.rstrip()}\n\n{body}"
else:
text = body
embedded.add(target_id)

return strip_path_placeholders(text), embedded


def _ref_candidates(ref: str) -> list[str]:
raw = str(ref or "").strip()
if not raw:
return []
out: list[str] = [raw]
if raw.startswith("[") and raw.endswith("]"):
inner = raw[1:-1].strip()
if inner and inner not in out:
out.append(inner)
else:
bracketed = f"[{raw}]"
if bracketed not in out:
out.append(bracketed)
return out
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Shared evidence_text rendering: one ``[E#]`` block per group.

Each block is ``[E#]`` + ``[§ path]`` (full traceable path) + body lines.
Groups are caller-provided; bodies in one group stay in that group.
"""

from __future__ import annotations

from typing import Sequence


def render_evidence_blocks(
groups: Sequence[tuple[str, Sequence[str]]],
*,
start_index: int = 1,
) -> str:
"""Render (path, bodies) groups into evidence_text.

``path`` is the full traceable header (e.g. file / section chain).
Bodies are joined with newlines; multiple bodies in one group are indented.
``start_index`` sets the first ``[E#]`` number (default 1).
"""
parts: list[str] = []
index = max(1, int(start_index or 1))
for path, bodies in groups:
texts = [str(t or "").strip() for t in bodies]
texts = [t for t in texts if t]
if not texts:
continue
block: list[str] = [f"[E{index + len(parts)}]"]
header = str(path or "").strip()
if header:
block.append(f"[§ {header}]")
indent = len(texts) >= 2
for text in texts:
if indent:
block.append(
"\n".join(
(" " + ln if ln.strip() else ln) for ln in text.splitlines()
)
)
else:
block.append(text)
parts.append("\n".join(block).strip())
return "\n\n".join(parts)

This file was deleted.

Loading
Loading