Newer
Older
MedSegAgentWebApp / webapp / backend / label_matcher.py
"""Resolve a free-text segmentation target (e.g. "liver") to a canonical
TotalSegmentator label name + integer id, using this repo's own
dataset/TotalSegmentator_v2.json (CT) / dataset/TotalSegmentator_MRI.json
(MRI) as the source of truth for the name<->id mapping.

Resolution order: exact match -> normalized match -> unique substring match
-> LLM disambiguation against the full label list.
"""

import json
import re

from . import config
from .query_parser import extract_json_from_output, get_oai_client_for_model


def _labels_path_for_modality(modality):
    return config.CT_LABELS_PATH if modality.upper().startswith("CT") else config.MRI_LABELS_PATH


def _load_labels(modality):
    path = _labels_path_for_modality(modality)
    with open(path, "r", encoding="utf-8") as f:
        data = json.load(f)
    return data["labels"]  # {name: int_id}, includes "background": 0


def _normalize(name):
    return re.sub(r"[\s\-]+", "_", name.strip().lower())


def _llm_disambiguate(target, labels, model):
    names = [n for n in labels.keys() if n != "background"]
    prompt = (
        "You are a medical imaging expert. Given the segmentation target "
        f'"{target}", pick the single closest matching name from this list '
        f"of available structures: {json.dumps(names, ensure_ascii=False)}\n"
        "Respond ONLY in this exact format:\n"
        'Final Answer: {"label": "chosen_name"}'
    )
    client = get_oai_client_for_model(model)
    response = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
    )
    text = response.choices[0].message.content or ""
    parsed = extract_json_from_output(text, expected_type="dict")
    label_name = parsed.get("label") if parsed else None
    if not label_name or label_name not in labels:
        raise ValueError(
            f"「{target}」に該当する構造が見つかりませんでした(LLM応答: {text!r})。"
        )
    return label_name


def resolve_target(target, modality, model):
    """Returns {"name": str, "id": int}. Raises ValueError if unresolved."""
    labels = _load_labels(modality)
    norm_target = _normalize(target)

    if norm_target in labels:
        return {"name": norm_target, "id": labels[norm_target]}

    candidates = [name for name in labels if norm_target in name or name in norm_target]
    candidates = [name for name in candidates if name != "background"]
    if len(candidates) == 1:
        return {"name": candidates[0], "id": labels[candidates[0]]}

    label_name = _llm_disambiguate(target, labels, model)
    return {"name": label_name, "id": labels[label_name]}