Spaces:
Sleeping
Sleeping
DocUA commited on
Commit ·
ea04df8
1
Parent(s): cc02af3
fix(l2b): units + document date from photo extraction; drop date/id-shaped values
Browse files
livemedcard/layers/l2b_nuextract.py
CHANGED
|
@@ -14,10 +14,17 @@ import json
|
|
| 14 |
from ..config import NUEXTRACT_MODEL, NUEXTRACT_URL
|
| 15 |
from ..llm_client import chat_completions
|
| 16 |
|
| 17 |
-
# Шаблон KVT-фактів (leaf-значення = типи NuExtract).
|
|
|
|
|
|
|
| 18 |
_FACTS_TEMPLATE = {
|
| 19 |
"facts": [
|
| 20 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
]
|
| 22 |
}
|
| 23 |
_TEMPLATE_JSON = json.dumps(_FACTS_TEMPLATE, indent=2)
|
|
|
|
| 14 |
from ..config import NUEXTRACT_MODEL, NUEXTRACT_URL
|
| 15 |
from ..llm_client import chat_completions
|
| 16 |
|
| 17 |
+
# Шаблон KVT-фактів (leaf-значення = типи NuExtract). Одиниця — ОКРЕМЕ поле, як у
|
| 18 |
+
# хмарного Lab: без неї значення приходить голим числом, і шар L2a не може
|
| 19 |
+
# порівняти його з референсними межами (натомість чесно піднімає unit_mismatch).
|
| 20 |
_FACTS_TEMPLATE = {
|
| 21 |
"facts": [
|
| 22 |
+
{
|
| 23 |
+
"keyword": "verbatim-string",
|
| 24 |
+
"value": "verbatim-string",
|
| 25 |
+
"unit": "verbatim-string",
|
| 26 |
+
"timestamp": "verbatim-string",
|
| 27 |
+
}
|
| 28 |
]
|
| 29 |
}
|
| 30 |
_TEMPLATE_JSON = json.dumps(_FACTS_TEMPLATE, indent=2)
|
livemedcard/layers/l2b_stage2_lora.py
CHANGED
|
@@ -102,6 +102,8 @@ def _resolve_loinc(keyword: str) -> tuple[str, str] | None:
|
|
| 102 |
|
| 103 |
_THOUGHT_TOKEN_RE = re.compile(r"<unused\d+>", re.IGNORECASE)
|
| 104 |
_VALUE_NUM_RE = re.compile(r"^\s*(-?\d+(?:[.,]\d+)?)\s*(.*)$")
|
|
|
|
|
|
|
| 105 |
_TS_DATE_FORMATS = ("%Y-%m-%d", "%d.%m.%Y", "%m/%d/%Y", "%B %d, %Y", "%b %d, %Y")
|
| 106 |
|
| 107 |
|
|
@@ -139,8 +141,15 @@ def _extract_json_array(text: str) -> list[dict]:
|
|
| 139 |
|
| 140 |
|
| 141 |
def _split_value(value: object) -> tuple[Optional[float], Optional[str]]:
|
| 142 |
-
"""'204 mg/dL' → (204.0, 'mg/dL'); '34%' → (34.0, '%'); 'Yes' → (None, None).
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
if m is None:
|
| 145 |
return None, None
|
| 146 |
num = float(m.group(1).replace(",", "."))
|
|
@@ -189,11 +198,36 @@ def extract_facts(summary_text: str, *, url: str | None = None, model: str | Non
|
|
| 189 |
return _extract_json_array(raw)
|
| 190 |
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
def to_observations(
|
| 193 |
kvt: list[dict], doc: Document, patient_id: str, *, extracted_by: str
|
| 194 |
) -> list[Observation]:
|
| 195 |
"""KVT4 → FHIR Observations (лише числові; часті лаби мостяться у LOINC)."""
|
| 196 |
subject = Reference(reference=f"Patient/{patient_id}")
|
|
|
|
| 197 |
observations: list[Observation] = []
|
| 198 |
seen: set[str] = set()
|
| 199 |
for item in kvt:
|
|
@@ -209,7 +243,11 @@ def to_observations(
|
|
| 209 |
if unit is None:
|
| 210 |
raw_unit = str(item.get("unit") or "").strip()
|
| 211 |
unit = raw_unit or None
|
| 212 |
-
eff, date_stated = _parse_timestamp(item.get("timestamp"),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
bridge = _resolve_loinc(keyword)
|
| 214 |
coding = (
|
| 215 |
[Coding(system=Sys.LOINC, code=bridge[0], display=bridge[1])]
|
|
|
|
| 102 |
|
| 103 |
_THOUGHT_TOKEN_RE = re.compile(r"<unused\d+>", re.IGNORECASE)
|
| 104 |
_VALUE_NUM_RE = re.compile(r"^\s*(-?\d+(?:[.,]\d+)?)\s*(.*)$")
|
| 105 |
+
# Дата або ідентифікатор: два розділювачі поспіль («2024-10-15», «06/13/2025»).
|
| 106 |
+
_DATE_LIKE_RE = re.compile(r"^\s*\d{1,4}[-/.]\d{1,2}[-/.]\d{2,4}")
|
| 107 |
_TS_DATE_FORMATS = ("%Y-%m-%d", "%d.%m.%Y", "%m/%d/%Y", "%B %d, %Y", "%b %d, %Y")
|
| 108 |
|
| 109 |
|
|
|
|
| 141 |
|
| 142 |
|
| 143 |
def _split_value(value: object) -> tuple[Optional[float], Optional[str]]:
|
| 144 |
+
"""'204 mg/dL' → (204.0, 'mg/dL'); '34%' → (34.0, '%'); 'Yes' → (None, None).
|
| 145 |
+
|
| 146 |
+
Дати й ідентифікатори — НЕ вимірювання: «2024-10-15» інакше стало б фактом
|
| 147 |
+
«2024.0 -10-15», а «2024-10-15/0412» — номером бланка в таймлайні.
|
| 148 |
+
"""
|
| 149 |
+
s = str(value)
|
| 150 |
+
if _DATE_LIKE_RE.match(s):
|
| 151 |
+
return None, None
|
| 152 |
+
m = _VALUE_NUM_RE.match(s)
|
| 153 |
if m is None:
|
| 154 |
return None, None
|
| 155 |
num = float(m.group(1).replace(",", "."))
|
|
|
|
| 198 |
return _extract_json_array(raw)
|
| 199 |
|
| 200 |
|
| 201 |
+
# Keywords, під якими модель віддає дату ЗАБОРУ матеріалу — тобто дату, на яку
|
| 202 |
+
# виміряні значення документа. Свідомо вузько: «reported»/«issued» — це коли
|
| 203 |
+
# лабораторія видала бланк, а не коли взяли кров, і датувати ним показник хибно.
|
| 204 |
+
_DOC_DATE_KEYWORDS = frozenset({
|
| 205 |
+
"collected", "collection date", "date collected", "specimen collected",
|
| 206 |
+
"дата забору", "забір матеріалу", "дата взяття",
|
| 207 |
+
})
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def _document_date(kvt: list[dict]) -> Optional[datetime]:
|
| 211 |
+
"""Дата документа з ЙОГО Ж витягу (напр. «Collected: 2024-10-15»).
|
| 212 |
+
|
| 213 |
+
Табличні бланки не повторюють дату в кожному рядку — вона стоїть у шапці, і
|
| 214 |
+
модель віддає її окремим фактом. Тоді значення документа датуються нею: це
|
| 215 |
+
дата, ЗАЯВЛЕНА в документі, а не здогад (на відміну від дати завантаження).
|
| 216 |
+
"""
|
| 217 |
+
for item in kvt:
|
| 218 |
+
if str(item.get("keyword", "")).strip().lower() in _DOC_DATE_KEYWORDS:
|
| 219 |
+
ts, ok = _parse_timestamp(item.get("value"), None)
|
| 220 |
+
if ok:
|
| 221 |
+
return ts
|
| 222 |
+
return None
|
| 223 |
+
|
| 224 |
+
|
| 225 |
def to_observations(
|
| 226 |
kvt: list[dict], doc: Document, patient_id: str, *, extracted_by: str
|
| 227 |
) -> list[Observation]:
|
| 228 |
"""KVT4 → FHIR Observations (лише числові; часті лаби мостяться у LOINC)."""
|
| 229 |
subject = Reference(reference=f"Patient/{patient_id}")
|
| 230 |
+
doc_date = _document_date(kvt)
|
| 231 |
observations: list[Observation] = []
|
| 232 |
seen: set[str] = set()
|
| 233 |
for item in kvt:
|
|
|
|
| 243 |
if unit is None:
|
| 244 |
raw_unit = str(item.get("unit") or "").strip()
|
| 245 |
unit = raw_unit or None
|
| 246 |
+
eff, date_stated = _parse_timestamp(item.get("timestamp"), None)
|
| 247 |
+
if not date_stated:
|
| 248 |
+
# дата на факт відсутня → дата документа; лише потім — дата завантаження
|
| 249 |
+
eff = doc_date or doc.received_at
|
| 250 |
+
date_stated = doc_date is not None
|
| 251 |
bridge = _resolve_loinc(keyword)
|
| 252 |
coding = (
|
| 253 |
[Coding(system=Sys.LOINC, code=bridge[0], display=bridge[1])]
|