Checkpoint frontend UI/UX overhaul and ingestion embed benchmark work

This commit is contained in:
2026-08-06 17:21:21 +07:00
parent 1e8cbdb586
commit a4b8e1c4db
78 changed files with 6761 additions and 654 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

@@ -0,0 +1,247 @@
"""Offline embedding benchmark aligned with deterministic drug resolution.
The production RAG contract never lets vector similarity choose a drug. This
benchmark therefore resolves the expected drug through the verified alias
catalog, filters candidates to that drug, and measures whether an embedding
retrieves the expected section. Policy/abstention and multi-drug cases are not
silently converted into retrieval cases.
Only BGE-M3 is accepted here. Cloud providers deliberately have no CLI switch;
using one requires a separate, explicitly approved benchmark path.
"""
from __future__ import annotations
import argparse
import csv
import json
import math
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Iterable, Mapping, Sequence
from ingestion.entities.catalog import normalize_name
from .local_bge_m3 import BgeM3Local
from .ports import EmbeddingProvider
ATTRIBUTE_TO_SECTION = {
"chong_chi_dinh": "chong_chi_dinh",
"lieu_dung": "lieu_luong_va_cach_dung",
"mang_thai": "thoi_ky_mang_thai",
"qua_lieu": "qua_lieu_va_xu_tri",
"tac_dung_phu": "tac_dung_khong_mong_muon",
"than_trong": "than_trong",
"tuong_tac": "tuong_tac_thuoc",
}
@dataclass(frozen=True)
class BenchmarkCase:
case_id: str
query: str
drug_id: str
expected_section: str
@dataclass(frozen=True)
class CaseResult:
case_id: str
drug_id: str
expected_section: str
first_relevant_rank: int | None
top_chunk_id: str
top_section: str
@dataclass(frozen=True)
class BenchmarkReport:
model_id: str
dimensions: int
case_count: int
candidate_chunk_count: int
document_latency_ms: float
query_latency_ms: float
document_requests: int
query_requests: int
hit_at_1: float
hit_at_3: float
hit_at_5: float
mrr: float
cases: list[CaseResult]
def _read_jsonl(path: Path) -> list[dict]:
with path.open(encoding="utf-8") as handle:
return [json.loads(line) for line in handle if line.strip()]
def _alias_lookup(path: Path) -> dict[str, set[str]]:
payload = json.loads(path.read_text(encoding="utf-8"))
lookup: dict[str, set[str]] = {}
for entity in payload["entities"]:
for alias in entity["aliases"]:
key = normalize_name(alias)
lookup.setdefault(key, set()).add(entity["drug_id"])
return lookup
def load_cases(golden_path: Path, entities_path: Path) -> list[BenchmarkCase]:
aliases = _alias_lookup(entities_path)
cases: list[BenchmarkCase] = []
with golden_path.open(encoding="utf-8-sig", newline="") as handle:
for row in csv.DictReader(handle):
attribute = row["thuoc_tinh_ky_vong"].strip()
drug_name = row["thuoc_ky_vong"].strip()
if not attribute or not drug_name or ";" in drug_name:
continue
section = ATTRIBUTE_TO_SECTION.get(attribute)
if section is None:
raise ValueError(
f"golden case {row['id']} has unmapped attribute {attribute!r}"
)
drug_ids = aliases.get(normalize_name(drug_name), set())
if not drug_ids:
raise ValueError(
f"golden case {row['id']} drug {drug_name!r} is not in the "
"verified entity catalog"
)
if len(drug_ids) != 1:
raise ValueError(
f"golden case {row['id']} drug {drug_name!r} is ambiguous: "
f"{sorted(drug_ids)}"
)
drug_id = next(iter(drug_ids))
cases.append(
BenchmarkCase(
case_id=row["id"],
query=row["cau_hoi"].strip(),
drug_id=drug_id,
expected_section=section,
)
)
if not cases:
raise ValueError("golden dataset has no single-drug retrieval cases")
return cases
def candidate_chunks(chunks_path: Path, cases: Sequence[BenchmarkCase]) -> list[dict]:
drug_ids = {case.drug_id for case in cases}
chunks = [
chunk
for chunk in _read_jsonl(chunks_path)
if chunk["drug_id"] in drug_ids and chunk["chunk_kind"] == "prose"
]
available = {(chunk["drug_id"], chunk["section_key"]) for chunk in chunks}
missing = [
case.case_id
for case in cases
if (case.drug_id, case.expected_section) not in available
]
if missing:
raise ValueError(f"golden cases have no canonical target chunks: {missing}")
return chunks
def _cosine(left: Sequence[float], right: Sequence[float]) -> float:
numerator = sum(a * b for a, b in zip(left, right, strict=True))
left_norm = math.sqrt(sum(value * value for value in left))
right_norm = math.sqrt(sum(value * value for value in right))
if left_norm == 0 or right_norm == 0:
raise ValueError("embedding benchmark received a zero vector")
return numerator / (left_norm * right_norm)
def run_benchmark(
provider: EmbeddingProvider,
cases: Sequence[BenchmarkCase],
chunks: Sequence[Mapping[str, object]],
) -> BenchmarkReport:
document_batch = provider.embed_documents([str(chunk["text"]) for chunk in chunks])
query_batch = provider.embed_queries([case.query for case in cases])
case_results: list[CaseResult] = []
for case, query_vector in zip(cases, query_batch.vectors, strict=True):
ranked = sorted(
(
(_cosine(query_vector.values, vector.values), chunk)
for chunk, vector in zip(chunks, document_batch.vectors, strict=True)
if chunk["drug_id"] == case.drug_id
),
key=lambda item: (-item[0], str(item[1]["chunk_id"])),
)
if not ranked:
raise ValueError(f"case {case.case_id} has no candidate chunks")
relevant_rank = next(
(
rank
for rank, (_, chunk) in enumerate(ranked, start=1)
if chunk["section_key"] == case.expected_section
),
None,
)
top = ranked[0][1]
case_results.append(
CaseResult(
case_id=case.case_id,
drug_id=case.drug_id,
expected_section=case.expected_section,
first_relevant_rank=relevant_rank,
top_chunk_id=str(top["chunk_id"]),
top_section=str(top["section_key"]),
)
)
count = len(case_results)
ranks = [result.first_relevant_rank for result in case_results]
def hit_at(limit: int) -> float:
return sum(rank is not None and rank <= limit for rank in ranks) / count
return BenchmarkReport(
model_id=provider.model_id,
dimensions=provider.dimensions,
case_count=count,
candidate_chunk_count=len(chunks),
document_latency_ms=document_batch.latency_ms,
query_latency_ms=query_batch.latency_ms,
document_requests=document_batch.request_count,
query_requests=query_batch.request_count,
hit_at_1=hit_at(1),
hit_at_3=hit_at(3),
hit_at_5=hit_at(5),
mrr=sum(0.0 if rank is None else 1.0 / rank for rank in ranks) / count,
cases=case_results,
)
def write_report(report: BenchmarkReport, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(asdict(report), ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
def main(argv: Iterable[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Local BGE-M3 subset benchmark")
parser.add_argument("--golden", type=Path, required=True)
parser.add_argument("--chunks", type=Path, required=True)
parser.add_argument("--entities", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--batch-size", type=int, default=8)
parser.add_argument("--device", default="cpu")
args = parser.parse_args(argv)
cases = load_cases(args.golden, args.entities)
chunks = candidate_chunks(args.chunks, cases)
report = run_benchmark(
BgeM3Local(batch_size=args.batch_size, device=args.device), cases, chunks
)
write_report(report, args.out)
print(json.dumps(asdict(report), ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,157 @@
import csv
import json
from pathlib import Path
import pytest
from ingestion.embed.benchmark_local import (
BenchmarkCase,
candidate_chunks,
load_cases,
run_benchmark,
)
from ingestion.embed.local_bge_m3 import BgeM3Local
def _write_csv(path: Path, rows: list[dict]) -> None:
with path.open("w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
def test_load_cases_uses_verified_aliases_and_skips_policy_and_multidrug(tmp_path):
entities = tmp_path / "entities.json"
entities.write_text(
json.dumps(
{
"entities": [
{"drug_id": "paracetamol", "aliases": ["Paracetamol"]}
]
}
),
encoding="utf-8",
)
golden = tmp_path / "golden.csv"
_write_csv(
golden,
[
{
"id": "1",
"cau_hoi": "Liều Paracetamol?",
"thuoc_ky_vong": "Paracetamol",
"thuoc_tinh_ky_vong": "lieu_dung",
},
{
"id": "2",
"cau_hoi": "Uống gì?",
"thuoc_ky_vong": "",
"thuoc_tinh_ky_vong": "",
},
{
"id": "3",
"cau_hoi": "So sánh",
"thuoc_ky_vong": "Paracetamol; Ibuprofen",
"thuoc_tinh_ky_vong": "lieu_dung",
},
],
)
assert load_cases(golden, entities) == [
BenchmarkCase(
case_id="1",
query="Liều Paracetamol?",
drug_id="paracetamol",
expected_section="lieu_luong_va_cach_dung",
)
]
def test_candidate_chunks_fails_when_ground_truth_is_missing(tmp_path):
path = tmp_path / "chunks.jsonl"
path.write_text(
json.dumps(
{
"drug_id": "drug",
"section_key": "chi_dinh",
"chunk_kind": "prose",
"text": "text",
}
)
+ "\n",
encoding="utf-8",
)
cases = [BenchmarkCase("1", "dose", "drug", "lieu_luong_va_cach_dung")]
with pytest.raises(ValueError, match="no canonical target"):
candidate_chunks(path, cases)
def test_load_cases_rejects_an_alias_ambiguous_in_the_verified_catalog(tmp_path):
entities = tmp_path / "entities.json"
entities.write_text(
json.dumps(
{
"entities": [
{"drug_id": "drug_a", "aliases": ["Shared"]},
{"drug_id": "drug_b", "aliases": ["Shared"]},
]
}
),
encoding="utf-8",
)
golden = tmp_path / "golden.csv"
_write_csv(
golden,
[
{
"id": "1",
"cau_hoi": "Liều Shared?",
"thuoc_ky_vong": "Shared",
"thuoc_tinh_ky_vong": "lieu_dung",
}
],
)
with pytest.raises(ValueError, match="is ambiguous"):
load_cases(golden, entities)
def test_benchmark_filters_by_drug_and_reports_first_relevant_rank():
vectors = {
"query": [1.0, 0.0],
"wrong section": [0.9, 0.1],
"correct section": [0.8, 0.2],
"other drug": [1.0, 0.0],
}
provider = BgeM3Local(encoder=lambda texts: [vectors[text] for text in texts])
provider._check_dimensions = lambda _values: None
cases = [BenchmarkCase("1", "query", "drug", "target")]
chunks = [
{
"chunk_id": "wrong",
"drug_id": "drug",
"section_key": "other",
"text": "wrong section",
},
{
"chunk_id": "right",
"drug_id": "drug",
"section_key": "target",
"text": "correct section",
},
{
"chunk_id": "leak",
"drug_id": "other",
"section_key": "target",
"text": "other drug",
},
]
report = run_benchmark(provider, cases, chunks)
assert report.case_count == 1
assert report.cases[0].first_relevant_rank == 2
assert report.hit_at_1 == 0.0
assert report.hit_at_3 == 1.0
assert report.mrr == 0.5