Cem13's picture
Release Kodama Vision with weights, runtime, attribution, and evaluation
ea2bc75 verified
Raw History Blame Contribute Delete
4.4 kB
"""Shared question and record types.
Public API types (Choice, Score, Noul) mirror Jev/Laya. Internally every question is
normalized to a `Question` with an ordered list of `Option`s so the data pipeline, the
serializer and the model all speak one format. Nothing here depends on torch.
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from typing import Literal, Union
QType = Literal["choice", "score", "noul"]
QTYPES: tuple[QType, ...] = ("choice", "score", "noul")
# ---------------------------------------------------------------- public API types
@dataclass
class Choice:
"""Pick one of several unordered options. criteria: {key: description} or [key, ...]."""
instructions: str
criteria: Union[dict[str, str], list[str]]
@dataclass
class Score:
"""Place the state on an ordered scale. criteria: level descriptions, lowest first (2-10)."""
instructions: str
criteria: list[str]
@dataclass
class Noul:
"""Probability that the statement in `instructions` holds for the state."""
instructions: str
PublicQuestion = Union[Choice, Score, Noul]
# ---------------------------------------------------------------- normalized form
@dataclass
class Option:
key: str # short label, e.g. "billing"; for score levels the level index as str
text: str # description; may be "" when the key says it all
@dataclass
class Question:
type: QType
instructions: str
options: list[Option] = field(default_factory=list) # empty for noul
@property
def n_options(self) -> int:
"""Number of scored options; noul is scored as two options (false, true)."""
return 2 if self.type == "noul" else len(self.options)
@classmethod
def from_public(cls, q: PublicQuestion) -> "Question":
if isinstance(q, Choice):
items = q.criteria.items() if isinstance(q.criteria, dict) else ((k, "") for k in q.criteria)
return cls("choice", q.instructions, [Option(str(k), str(v)) for k, v in items])
if isinstance(q, Score):
return cls("score", q.instructions, [Option(str(i), t) for i, t in enumerate(q.criteria)])
if isinstance(q, Noul):
return cls("noul", q.instructions, [])
raise TypeError(f"not a question type: {type(q).__name__}")
def validate(self) -> None:
assert self.type in QTYPES, self.type
if self.type == "noul":
assert not self.options, "noul questions have no options"
elif self.type == "score":
assert 2 <= len(self.options) <= 10, "score needs 2-10 levels"
else:
assert 2 <= len(self.options) <= 255, "choice needs 2-255 options"
keys = [o.key for o in self.options]
assert len(set(keys)) == len(keys), "choice keys must be unique"
# ---------------------------------------------------------------- training record
@dataclass
class Record:
"""One (state, question, gold answer) training/eval example = one JSONL line.
label: index into question.options for choice/score; 0 (false) or 1 (true) for noul.
"""
id: str
source: str # dataset of origin, e.g. "ag_news"
state: str
question: Question
label: int
def validate(self) -> None:
self.question.validate()
assert 0 <= self.label < self.question.n_options, (self.id, self.label)
assert self.state.strip(), self.id
def to_json(self) -> str:
q = self.question
return json.dumps({
"id": self.id, "source": self.source, "state": self.state,
"type": q.type, "instructions": q.instructions,
"options": [{"key": o.key, "text": o.text} for o in q.options],
"label": self.label,
}, ensure_ascii=False)
@classmethod
def from_json(cls, line: str) -> "Record":
d = json.loads(line)
q = Question(d["type"], d["instructions"], [Option(o["key"], o["text"]) for o in d["options"]])
return cls(d["id"], d["source"], d["state"], q, int(d["label"]))
def read_jsonl(path: str) -> list[Record]:
with open(path, encoding="utf-8") as f:
return [Record.from_json(line) for line in f if line.strip()]
def write_jsonl(path: str, records: list[Record]) -> None:
with open(path, "w", encoding="utf-8") as f:
for r in records:
f.write(r.to_json() + "\n")