File size: 3,210 Bytes
580cc4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
"""The Jev interface, reimplemented.

A *state* (any JSON or text) plus N *typed questions*. One forward pass answers
all of them. Three primitive types, matching Jev:

  noul   - boolean          criteria: {"true": desc, "false": desc}
  choice - enum             criteria: {label: desc, ...}
  score  - ordered scale    criteria: [desc_0, desc_1, ...]   labels are "0".."n-1"

Everything downstream reads questions through `iter_labels`, so adding a new
primitive means touching this file only.
"""
from __future__ import annotations
import json
from typing import Any

TYPES = ["noul", "choice", "score"]
TYPE_ID = {t: i for i, t in enumerate(TYPES)}


# A bare noul carries no criteria at all - the instruction is the statement and
# the labels are implicit. 10% of typed-decisions is shaped this way, so treat it
# as part of the format rather than as malformed input.
BARE_NOUL = [("false", "The statement does not hold."),
             ("true", "The statement holds.")]


def iter_labels(q: dict) -> list[tuple[str, str]]:
    """Normalize a question's criteria to an ordered [(label, description)] list.

    `score` criteria arrive as an ordered list; its labels are the indices as
    strings, which is how the gold distributions key them.
    """
    crit = q.get("criteria")
    if crit is None:
        if q["type"] != "noul":
            raise ValueError(
                f"a {q['type']} question needs criteria - only noul may omit "
                f"them: {q.get('instructions', '')[:80]!r}")
        return list(BARE_NOUL)
    if isinstance(crit, list):                      # score
        return [(str(i), d) for i, d in enumerate(crit)]
    if q["type"] == "noul":                         # pin order: false, true
        return [(k, crit[k]) for k in ("false", "true") if k in crit]
    return sorted(crit.items())                     # choice - stable order


def state_to_text(state: Any) -> str:
    if isinstance(state, str):
        return state
    return json.dumps(state, indent=1, sort_keys=True, ensure_ascii=False)


def label_text(label: str, desc: str) -> str:
    return f"{label}: {desc}"


class Question(dict):
    """Convenience builder so you can define a schema in code.

    >>> Question.choice("What should we do?", {"retry": "...", "stop": "..."})
    """

    @staticmethod
    def noul(instructions: str, true_desc: str = None,
             false_desc: str = None) -> dict:
        """Descriptions are optional, matching the data: a bare noul carries no
        criteria at all and the instruction is the statement being judged."""
        q = {"type": "noul", "instructions": instructions}
        if true_desc is not None or false_desc is not None:
            q["criteria"] = {"true": true_desc or "The statement holds.",
                             "false": false_desc or "The statement does not hold."}
        return q

    @staticmethod
    def choice(instructions: str, criteria: dict[str, str]) -> dict:
        return {"type": "choice", "instructions": instructions, "criteria": criteria}

    @staticmethod
    def score(instructions: str, levels: list[str]) -> dict:
        return {"type": "score", "instructions": instructions, "criteria": levels}