316 lines
7.1 KiB
Python
316 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from evaluation.retrieval_runner import filter_questions_by_split
|
|
|
|
|
|
DEFAULT_DELAY = 0.5
|
|
|
|
ALLOWED_OVERRIDE_FIELDS = {
|
|
"question",
|
|
"expected_answer_contains",
|
|
"expected_source_urls",
|
|
"should_answer",
|
|
"note",
|
|
}
|
|
|
|
|
|
def apply_question_overrides(
|
|
questions: list[dict[str, Any]],
|
|
path: Path,
|
|
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
if not path.exists():
|
|
raise FileNotFoundError(
|
|
f"RAG answer override súbor neexistuje: {path}"
|
|
)
|
|
|
|
with path.open("r", encoding="utf-8") as file:
|
|
overrides = json.load(file)
|
|
|
|
if not isinstance(overrides, dict):
|
|
raise ValueError(
|
|
"rag_answer_overrides.json musí obsahovať JSON objekt."
|
|
)
|
|
|
|
known_ids = {str(item["id"]) for item in questions}
|
|
unknown_ids = set(overrides.keys()) - known_ids
|
|
|
|
if unknown_ids:
|
|
raise ValueError(
|
|
"Override súbor obsahuje neznáme question ID: "
|
|
+ ", ".join(sorted(unknown_ids))
|
|
)
|
|
|
|
result: list[dict[str, Any]] = []
|
|
applied_ids: list[str] = []
|
|
|
|
for question in questions:
|
|
item = dict(question)
|
|
question_id = str(item["id"])
|
|
override = overrides.get(question_id)
|
|
|
|
if override is None:
|
|
result.append(item)
|
|
continue
|
|
|
|
if not isinstance(override, dict):
|
|
raise ValueError(
|
|
f"{question_id}: override musí byť JSON objekt."
|
|
)
|
|
|
|
unsupported_fields = (
|
|
set(override.keys())
|
|
- ALLOWED_OVERRIDE_FIELDS
|
|
)
|
|
|
|
if unsupported_fields:
|
|
raise ValueError(
|
|
f"{question_id}: nepovolené override polia: "
|
|
+ ", ".join(
|
|
sorted(unsupported_fields)
|
|
)
|
|
)
|
|
|
|
if "question" in override:
|
|
overridden_question = override["question"]
|
|
|
|
if (
|
|
not isinstance(
|
|
overridden_question,
|
|
str,
|
|
)
|
|
or not overridden_question.strip()
|
|
):
|
|
raise ValueError(
|
|
f"{question_id}: "
|
|
"override question musí byť "
|
|
"neprázdny reťazec."
|
|
)
|
|
|
|
item.update(override)
|
|
result.append(item)
|
|
applied_ids.append(question_id)
|
|
|
|
return result, applied_ids
|
|
|
|
|
|
def validate_string_list(
|
|
question_id: str,
|
|
field_name: str,
|
|
value: Any,
|
|
) -> None:
|
|
if not isinstance(value, list):
|
|
raise ValueError(
|
|
f"{question_id}: "
|
|
f"{field_name} musí byť JSON pole."
|
|
)
|
|
|
|
for index, item in enumerate(value):
|
|
if not isinstance(item, str):
|
|
raise ValueError(
|
|
f"{question_id}: "
|
|
f"{field_name}[{index}] "
|
|
"musí byť reťazec."
|
|
)
|
|
|
|
|
|
def validate_answer_questions(
|
|
questions: list[dict[str, Any]],
|
|
) -> None:
|
|
seen_ids: set[str] = set()
|
|
|
|
for index, question in enumerate(
|
|
questions,
|
|
start=1,
|
|
):
|
|
if not isinstance(question, dict):
|
|
raise ValueError(
|
|
f"Otázka #{index} "
|
|
"musí byť JSON objekt."
|
|
)
|
|
|
|
question_id = question.get("id")
|
|
|
|
if (
|
|
not isinstance(question_id, str)
|
|
or not question_id.strip()
|
|
):
|
|
raise ValueError(
|
|
f"Otázka #{index}: "
|
|
"id musí byť neprázdny reťazec."
|
|
)
|
|
|
|
if question_id in seen_ids:
|
|
raise ValueError(
|
|
f"Duplicitné question ID: {question_id}"
|
|
)
|
|
|
|
seen_ids.add(question_id)
|
|
|
|
split = question.get("split")
|
|
|
|
if split not in {
|
|
"dev",
|
|
"test",
|
|
}:
|
|
raise ValueError(
|
|
f"{question_id}: "
|
|
"split musí byť "
|
|
"'dev' alebo 'test'."
|
|
)
|
|
|
|
question_text = question.get(
|
|
"question"
|
|
)
|
|
|
|
if (
|
|
not isinstance(
|
|
question_text,
|
|
str,
|
|
)
|
|
or not question_text.strip()
|
|
):
|
|
raise ValueError(
|
|
f"{question_id}: "
|
|
"question musí byť "
|
|
"neprázdny reťazec."
|
|
)
|
|
|
|
validate_string_list(
|
|
question_id,
|
|
"expected_documents",
|
|
question.get(
|
|
"expected_documents"
|
|
),
|
|
)
|
|
|
|
validate_string_list(
|
|
question_id,
|
|
"expected_source_urls",
|
|
question.get(
|
|
"expected_source_urls"
|
|
),
|
|
)
|
|
|
|
validate_string_list(
|
|
question_id,
|
|
"expected_answer_contains",
|
|
question.get(
|
|
"expected_answer_contains"
|
|
),
|
|
)
|
|
|
|
if not isinstance(
|
|
question.get(
|
|
"should_answer"
|
|
),
|
|
bool,
|
|
):
|
|
raise ValueError(
|
|
f"{question_id}: "
|
|
"should_answer musí byť boolean."
|
|
)
|
|
|
|
for field_name in (
|
|
"category",
|
|
"difficulty",
|
|
):
|
|
value = question.get(
|
|
field_name
|
|
)
|
|
|
|
if (
|
|
value is not None
|
|
and not isinstance(
|
|
value,
|
|
str,
|
|
)
|
|
):
|
|
raise ValueError(
|
|
f"{question_id}: "
|
|
f"{field_name} musí byť "
|
|
"reťazec."
|
|
)
|
|
|
|
note = question.get("note")
|
|
|
|
if (
|
|
note is not None
|
|
and not isinstance(
|
|
note,
|
|
str,
|
|
)
|
|
):
|
|
raise ValueError(
|
|
f"{question_id}: "
|
|
"note musí byť reťazec "
|
|
"alebo null."
|
|
)
|
|
|
|
|
|
def select_questions(
|
|
questions: list[dict[str, Any]],
|
|
*,
|
|
split: str,
|
|
question_ids: list[str],
|
|
limit: int | None,
|
|
) -> list[dict[str, Any]]:
|
|
selected = filter_questions_by_split(
|
|
questions,
|
|
split,
|
|
)
|
|
|
|
if question_ids:
|
|
wanted = set(question_ids)
|
|
|
|
selected = [
|
|
item
|
|
for item in selected
|
|
if item.get("id") in wanted
|
|
]
|
|
|
|
if limit is not None:
|
|
selected = selected[:limit]
|
|
|
|
return selected
|
|
|
|
|
|
def result_prefix(
|
|
*,
|
|
split: str,
|
|
limit: int | None,
|
|
question_ids: list[str],
|
|
) -> str:
|
|
prefix = f"rag_answers_{split}"
|
|
|
|
if question_ids:
|
|
return prefix + "_selected"
|
|
|
|
if limit is not None:
|
|
return prefix + f"_limit{limit}"
|
|
|
|
return prefix
|
|
|
|
|
|
def questions_fingerprint(
|
|
questions: list[dict[str, Any]],
|
|
) -> str:
|
|
canonical = json.dumps(
|
|
questions,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(
|
|
",",
|
|
":",
|
|
),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
|
|
return hashlib.sha256(
|
|
canonical
|
|
).hexdigest()
|