urim 0.0.1a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- urim/__init__.py +60 -0
- urim/__main__.py +7 -0
- urim/adapters.py +80 -0
- urim/boundary.py +100 -0
- urim/camps/__init__.py +81 -0
- urim/camps/decisions.py +89 -0
- urim/camps/shared.py +124 -0
- urim/camps/systemone.py +71 -0
- urim/canonical.py +18 -0
- urim/classify.py +55 -0
- urim/cli.py +283 -0
- urim/client.py +430 -0
- urim/cuts.py +45 -0
- urim/define.py +96 -0
- urim/descriptors.py +214 -0
- urim/diff.py +28 -0
- urim/emit.py +50 -0
- urim/envelope.py +106 -0
- urim/envelope_read.py +63 -0
- urim/errors.py +91 -0
- urim/fit.py +251 -0
- urim/fit_math.py +119 -0
- urim/freeze.py +24 -0
- urim/jsonfmt.py +120 -0
- urim/lock.py +89 -0
- urim/plan.py +55 -0
- urim/providers.py +95 -0
- urim/py.typed +0 -0
- urim/schema.py +47 -0
- urim/testing.py +72 -0
- urim/types.py +322 -0
- urim-0.0.1a1.dist-info/METADATA +105 -0
- urim-0.0.1a1.dist-info/RECORD +38 -0
- urim-0.0.1a1.dist-info/WHEEL +5 -0
- urim-0.0.1a1.dist-info/entry_points.txt +2 -0
- urim-0.0.1a1.dist-info/licenses/LICENSE +202 -0
- urim-0.0.1a1.dist-info/licenses/NOTICE +4 -0
- urim-0.0.1a1.dist-info/top_level.txt +1 -0
urim/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""urim (Python port). Same canonical JSON, hashes and envelope as the TypeScript port."""
|
|
2
|
+
|
|
3
|
+
from .camps import decode_reply, encode_request, request_id_of
|
|
4
|
+
from .canonical import canonicalize, sha256
|
|
5
|
+
from .classify import classify_reply, classify_vendor_reply
|
|
6
|
+
from .client import Client, create_client
|
|
7
|
+
from .cuts import resolve_cuts
|
|
8
|
+
from .define import choice, define_decision, noul, score, threshold, tier
|
|
9
|
+
from .descriptors import DESCRIPTORS, GENERIC_ERRORS, descriptor_for, model_spec_for
|
|
10
|
+
from .emit import emit
|
|
11
|
+
from .envelope import build_envelope
|
|
12
|
+
from .envelope_read import read_envelope
|
|
13
|
+
from .errors import FAILOVER_TYPES, Issue, UrimError
|
|
14
|
+
from .fit import estimate_fit_cost, parse_cases, run_fit
|
|
15
|
+
from .lock import parse_lock, serialize_lock
|
|
16
|
+
from .plan import estimate_input_tokens, explain, next_step, plan_attempts
|
|
17
|
+
from .schema import json_schema_adapter, pydantic_schema
|
|
18
|
+
from .types import CONFIDENCE_RULE, Objective
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"CONFIDENCE_RULE",
|
|
22
|
+
"DESCRIPTORS",
|
|
23
|
+
"FAILOVER_TYPES",
|
|
24
|
+
"GENERIC_ERRORS",
|
|
25
|
+
"Client",
|
|
26
|
+
"Issue",
|
|
27
|
+
"Objective",
|
|
28
|
+
"UrimError",
|
|
29
|
+
"build_envelope",
|
|
30
|
+
"canonicalize",
|
|
31
|
+
"choice",
|
|
32
|
+
"classify_reply",
|
|
33
|
+
"classify_vendor_reply",
|
|
34
|
+
"create_client",
|
|
35
|
+
"decode_reply",
|
|
36
|
+
"define_decision",
|
|
37
|
+
"descriptor_for",
|
|
38
|
+
"emit",
|
|
39
|
+
"encode_request",
|
|
40
|
+
"estimate_fit_cost",
|
|
41
|
+
"estimate_input_tokens",
|
|
42
|
+
"explain",
|
|
43
|
+
"json_schema_adapter",
|
|
44
|
+
"model_spec_for",
|
|
45
|
+
"next_step",
|
|
46
|
+
"noul",
|
|
47
|
+
"parse_cases",
|
|
48
|
+
"parse_lock",
|
|
49
|
+
"plan_attempts",
|
|
50
|
+
"pydantic_schema",
|
|
51
|
+
"read_envelope",
|
|
52
|
+
"request_id_of",
|
|
53
|
+
"resolve_cuts",
|
|
54
|
+
"run_fit",
|
|
55
|
+
"score",
|
|
56
|
+
"serialize_lock",
|
|
57
|
+
"sha256",
|
|
58
|
+
"threshold",
|
|
59
|
+
"tier",
|
|
60
|
+
]
|
urim/__main__.py
ADDED
urim/adapters.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""A: real HTTP adapters over httpx (opt-in: ``from urim.adapters import typesafe_jev, openai_decisions``).
|
|
2
|
+
|
|
3
|
+
Nothing here runs unless you import it AND pass it to ``create_client``. The credential arrives from the client,
|
|
4
|
+
which reads the env var named by ``credential_env`` at send time. It is put in one request header and never
|
|
5
|
+
logged, stored, or placed in any error message. Pass ``create_client(http=httpx.AsyncClient(...))`` to share a
|
|
6
|
+
connection pool (tests use ``httpx.MockTransport``); otherwise one client is opened per send.
|
|
7
|
+
Requires the optional extra: ``pip install urim[httpx]``. Mirrors urim-ts src/adapters/index.ts.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .descriptors import DESCRIPTORS, AuthSpec, Descriptor
|
|
18
|
+
from .errors import UrimError
|
|
19
|
+
from .jsonfmt import stringify
|
|
20
|
+
from .testing import SendIO
|
|
21
|
+
from .types import WireReply, WireRequest
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _reject_constant(name: str) -> Any:
|
|
25
|
+
raise ValueError(name) # NaN/Infinity are not JSON (JSON.parse rejects them too)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _read_body(text: str) -> Any:
|
|
29
|
+
try:
|
|
30
|
+
return json.loads(text, parse_constant=_reject_constant)
|
|
31
|
+
except ValueError:
|
|
32
|
+
return text # non-JSON -> the decoder reports nonconformant_response
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class HttpAdapter:
|
|
37
|
+
provider: str
|
|
38
|
+
credential_env: str
|
|
39
|
+
_url: str
|
|
40
|
+
_auth: AuthSpec
|
|
41
|
+
|
|
42
|
+
async def send(self, req: WireRequest, io: SendIO) -> WireReply:
|
|
43
|
+
if io.credential == "":
|
|
44
|
+
raise UrimError("auth", f"missing credential: set {self.credential_env}", tier=req.tier_id)
|
|
45
|
+
value = io.credential if self._auth.scheme is None else f"{self._auth.scheme} {io.credential}"
|
|
46
|
+
headers = {"content-type": "application/json", self._auth.header: value}
|
|
47
|
+
content = stringify(req.body).encode("utf-8")
|
|
48
|
+
started = time.monotonic()
|
|
49
|
+
if io.http is not None:
|
|
50
|
+
res = await io.http.post(req.url, content=content, headers=headers)
|
|
51
|
+
else:
|
|
52
|
+
import httpx # optional extra, imported only when a real send happens
|
|
53
|
+
|
|
54
|
+
async with httpx.AsyncClient() as http:
|
|
55
|
+
res = await http.post(req.url, content=content, headers=headers)
|
|
56
|
+
return WireReply(
|
|
57
|
+
res.status_code,
|
|
58
|
+
{k.lower(): v for k, v in res.headers.items()},
|
|
59
|
+
_read_body(res.text),
|
|
60
|
+
(time.monotonic() - started) * 1000,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def http_adapter(d: Descriptor) -> HttpAdapter:
|
|
65
|
+
"""One adapter per descriptor whose URL needs no caller-supplied placeholders."""
|
|
66
|
+
if not d.auth or "{" in d.url:
|
|
67
|
+
raise UrimError(
|
|
68
|
+
"invalid_definition", f"{d.provider}: descriptor needs URL/auth options this slice does not support"
|
|
69
|
+
)
|
|
70
|
+
return HttpAdapter(d.provider, d.auth[0].env, d.url, d.auth[0])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def typesafe_jev() -> HttpAdapter:
|
|
74
|
+
"""TypeSafe Jev, systemone camp. Bearer TYPESAFE_API_KEY -> the descriptor URL."""
|
|
75
|
+
return http_adapter(DESCRIPTORS["typesafe"])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def openai_decisions() -> HttpAdapter:
|
|
79
|
+
"""OpenAI, decisions camp. Bearer OPENAI_API_KEY -> the descriptor URL."""
|
|
80
|
+
return http_adapter(DESCRIPTORS["openai"])
|
urim/boundary.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""C: the ONE place Python snake_case meets the camelCase JSON urim-ts writes (contract, lock, envelope, settled).
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
- Only dataclass attribute names are mapped. Mapping keys (question names, tier ids, threshold names, vendor
|
|
5
|
+
``native`` data, raw bodies) are user/vendor data and are never re-cased.
|
|
6
|
+
- ``json_name``: snake -> lowerCamel for every member (no exceptions since slice 7), plus keyword escapes (``from_``).
|
|
7
|
+
- ``ABSENT`` attributes are omitted; ``interpret_failed`` is written only when true (as in urim-ts).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import dataclasses
|
|
13
|
+
import types as _pytypes
|
|
14
|
+
import typing
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from functools import cache
|
|
17
|
+
from types import MappingProxyType
|
|
18
|
+
from typing import Any, Literal, TypeVar, Union
|
|
19
|
+
|
|
20
|
+
from .freeze import deep_freeze
|
|
21
|
+
from .types import ABSENT, PriceTable, _Absent
|
|
22
|
+
|
|
23
|
+
_T = TypeVar("_T")
|
|
24
|
+
_ESCAPES = {"from_": "from"}
|
|
25
|
+
_OMIT_IF_FALSE = frozenset({"interpret_failed"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@cache
|
|
29
|
+
def json_name(attr: str) -> str:
|
|
30
|
+
"""The JSON member name for dataclass attribute ``attr``."""
|
|
31
|
+
if attr in _ESCAPES:
|
|
32
|
+
return _ESCAPES[attr]
|
|
33
|
+
if "_" not in attr:
|
|
34
|
+
return attr
|
|
35
|
+
head, *rest = attr.split("_")
|
|
36
|
+
return head + "".join(p[:1].upper() + p[1:] for p in rest)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _omitted(name: str, value: Any) -> bool:
|
|
40
|
+
return value is ABSENT or (name in _OMIT_IF_FALSE and value is False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def to_json(value: Any) -> Any:
|
|
44
|
+
"""Python data -> plain JSON-ready data with urim-ts member names."""
|
|
45
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
46
|
+
return {
|
|
47
|
+
json_name(f.name): to_json(v)
|
|
48
|
+
for f in dataclasses.fields(value)
|
|
49
|
+
if not _omitted(f.name, v := getattr(value, f.name))
|
|
50
|
+
}
|
|
51
|
+
if isinstance(value, Mapping):
|
|
52
|
+
return {k: to_json(v) for k, v in value.items()}
|
|
53
|
+
if isinstance(value, (list, tuple)):
|
|
54
|
+
return [to_json(v) for v in value]
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@cache
|
|
59
|
+
def _hints(cls: type) -> dict[str, Any]:
|
|
60
|
+
return typing.get_type_hints(cls)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _convert(tp: Any, v: Any) -> Any:
|
|
64
|
+
origin = typing.get_origin(tp)
|
|
65
|
+
if origin in (Union, _pytypes.UnionType):
|
|
66
|
+
args = [a for a in typing.get_args(tp) if a is not _Absent]
|
|
67
|
+
if v is None and type(None) in args:
|
|
68
|
+
return None
|
|
69
|
+
concrete = [a for a in args if a is not type(None)]
|
|
70
|
+
return _convert(concrete[0], v) if len(concrete) == 1 else deep_freeze(v)
|
|
71
|
+
if isinstance(tp, type) and dataclasses.is_dataclass(tp):
|
|
72
|
+
return from_json(tp, v)
|
|
73
|
+
if origin is tuple:
|
|
74
|
+
(item, *_rest) = typing.get_args(tp)
|
|
75
|
+
return tuple(_convert(item, x) for x in v)
|
|
76
|
+
if origin in (Mapping, dict) or tp is Mapping:
|
|
77
|
+
kv = typing.get_args(tp)
|
|
78
|
+
return MappingProxyType({k: _convert(kv[1] if kv else Any, x) for k, x in v.items()})
|
|
79
|
+
if origin is Literal or tp in (str, int, float, bool):
|
|
80
|
+
return v
|
|
81
|
+
return deep_freeze(v)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def from_json(cls: type[_T], data: Mapping[str, Any]) -> _T:
|
|
85
|
+
"""Plain JSON (already validated by its reader) -> frozen snake_case dataclass. Unknown members ignored."""
|
|
86
|
+
dc: Any = cls # a dataclass type; the checker cannot see that through a TypeVar
|
|
87
|
+
hints = _hints(dc)
|
|
88
|
+
kwargs = {}
|
|
89
|
+
for f in dataclasses.fields(dc):
|
|
90
|
+
key = json_name(f.name)
|
|
91
|
+
if key in data:
|
|
92
|
+
kwargs[f.name] = _convert(hints[f.name], data[key])
|
|
93
|
+
elif f.default is dataclasses.MISSING:
|
|
94
|
+
raise KeyError(f"{cls.__name__}: missing member {key!r}")
|
|
95
|
+
return cls(**kwargs)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def parse_price_table(data: Mapping[str, Any]) -> PriceTable:
|
|
99
|
+
"""The shared PriceTable JSON (camelCase, as urim-ts) -> PriceTable."""
|
|
100
|
+
return from_json(PriceTable, data)
|
urim/camps/__init__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""C: camp dispatch. URL, wire model, error map, aliases and request-id header all come from descriptor data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Protocol
|
|
6
|
+
|
|
7
|
+
from ..classify import classify_vendor_reply
|
|
8
|
+
from ..descriptors import descriptor_for, explicit_model_spec, header, model_spec_for
|
|
9
|
+
from ..errors import UrimError
|
|
10
|
+
from ..types import Definition, Tier, WireReply, WireRequest
|
|
11
|
+
from . import decisions, systemone
|
|
12
|
+
from .shared import Decoded, DecodedFail, DecodedOk, TokenUsage
|
|
13
|
+
|
|
14
|
+
__all__ = ["Decoded", "DecodedFail", "DecodedOk", "TokenUsage", "decode_reply", "encode_request", "request_id_of"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _Codec(Protocol):
|
|
18
|
+
PATH: str
|
|
19
|
+
|
|
20
|
+
def encode(self, d: Definition, wire: str, input: Any) -> Any: ...
|
|
21
|
+
def decode(self, d: Definition, tier: Tier, body: Any, *, unavailable_sentinel: bool = False) -> Decoded: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _codec(tier: Tier) -> _Codec:
|
|
25
|
+
return systemone if tier.camp == "systemone" else decisions
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def encode_request(d: Definition, tier: Tier, input: Any) -> WireRequest:
|
|
29
|
+
desc = descriptor_for(tier)
|
|
30
|
+
wire = model_spec_for(desc, tier).wire
|
|
31
|
+
codec = _codec(tier)
|
|
32
|
+
return WireRequest(tier.id, tier.camp, codec.PATH, desc.url.replace("{model}", wire), codec.encode(d, wire, input))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def request_id_of(tier: Tier, reply: WireReply) -> str | None:
|
|
36
|
+
name = descriptor_for(tier).request_id_header
|
|
37
|
+
return None if name is None else header(reply.headers, name)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _with_request_id(e: UrimError, rid: str | None) -> UrimError:
|
|
41
|
+
if rid is None or e.request_id is not None:
|
|
42
|
+
return e
|
|
43
|
+
return UrimError(
|
|
44
|
+
e.type,
|
|
45
|
+
e.message,
|
|
46
|
+
tier=e.tier,
|
|
47
|
+
status=e.status,
|
|
48
|
+
request_id=rid,
|
|
49
|
+
retry_after_ms=e.retry_after_ms,
|
|
50
|
+
issues=e.issues,
|
|
51
|
+
cause=e.__cause__,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def decode_reply(d: Definition, tier: Tier, reply: WireReply) -> Decoded:
|
|
56
|
+
desc = descriptor_for(tier)
|
|
57
|
+
rid = request_id_of(tier, reply)
|
|
58
|
+
failure = classify_vendor_reply(desc, reply)
|
|
59
|
+
if failure.type is not None:
|
|
60
|
+
return DecodedFail(
|
|
61
|
+
UrimError(
|
|
62
|
+
failure.type,
|
|
63
|
+
f"HTTP {reply.status} from {tier.id}",
|
|
64
|
+
tier=tier.id,
|
|
65
|
+
status=reply.status,
|
|
66
|
+
request_id=rid,
|
|
67
|
+
retry_after_ms=failure.retry_after_ms,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
decoded = _codec(tier).decode(d, tier, reply.body, unavailable_sentinel=desc.confidence_unavailable_sentinel)
|
|
71
|
+
if isinstance(decoded, DecodedFail):
|
|
72
|
+
return DecodedFail(_with_request_id(decoded.error, rid))
|
|
73
|
+
# Only tiers with an explicit descriptor model row are checked (accepted 2026-10-28).
|
|
74
|
+
spec = explicit_model_spec(desc, tier)
|
|
75
|
+
if spec is not None and decoded.model is not None and decoded.model not in spec.answered_as:
|
|
76
|
+
return DecodedFail(
|
|
77
|
+
UrimError(
|
|
78
|
+
"lock_violation", f'{tier.id} was answered by model "{decoded.model}"', tier=tier.id, request_id=rid
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
return decoded
|
urim/camps/decisions.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""C: the OpenAI decisions body. Sources as in urim-ts src/camps/decisions.ts (checked 2026-10-28 EDT).
|
|
2
|
+
TODO(vendor-fact): OpenAI's confidence formula is undocumented; native choice/score confidence passes through."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..errors import UrimError
|
|
9
|
+
from ..types import Definition, Question, Tier
|
|
10
|
+
from .shared import (
|
|
11
|
+
Decoded,
|
|
12
|
+
DecodedFail,
|
|
13
|
+
RawAnswer,
|
|
14
|
+
argmax,
|
|
15
|
+
binary_confidence,
|
|
16
|
+
binary_label,
|
|
17
|
+
nonconformant,
|
|
18
|
+
num,
|
|
19
|
+
to_answers,
|
|
20
|
+
vendor_confidence,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
PATH = "/v1/decisions"
|
|
24
|
+
_NATIVE_TYPE = {"noul": "predicate", "choice": "choice", "score": "score"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _encode_question(name: str, q: Question) -> dict[str, Any]:
|
|
28
|
+
base: dict[str, Any] = {"type": _NATIVE_TYPE[q.type], "name": name, "instructions": q.prompt}
|
|
29
|
+
if q.type == "choice":
|
|
30
|
+
return {**base, "choices": [{"value": v, "description": t} for v, t in q.options.items()]}
|
|
31
|
+
if q.type == "score":
|
|
32
|
+
return {**base, "levels": [{"label": v, "description": t} for v, t in q.options.items()]}
|
|
33
|
+
return base # predicates carry no yes/no descriptions (O2; accepted 2026-10-28)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def encode(d: Definition, wire_model: str, input: Any) -> Any:
|
|
37
|
+
return {
|
|
38
|
+
"model": wire_model,
|
|
39
|
+
# TODO(vendor-fact): exact message item wrapper is medium confidence.
|
|
40
|
+
"input": [{"role": "user", "content": [{"type": "input_json", "data": input}]}],
|
|
41
|
+
"questions": [_encode_question(n, q) for n, q in d.questions.items()],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _decode_one(name: str, a: dict[str, Any], sentinel: bool) -> RawAnswer | None:
|
|
46
|
+
t = a.get("type")
|
|
47
|
+
if t == "predicate":
|
|
48
|
+
p = num(a.get("probability"))
|
|
49
|
+
# OpenAI returns no predicate confidence: Urim derives |2p - 1| and says so (urim_derived).
|
|
50
|
+
return None if p is None else RawAnswer(name, binary_label(p), binary_confidence(p), p, None, "urim_derived")
|
|
51
|
+
if t == "choice": # a boolean choice never equals a string label -> nonconformant later
|
|
52
|
+
return RawAnswer(
|
|
53
|
+
name, a.get("choice"), vendor_confidence(a, sentinel), a.get("choice"), a.get("confidence"), "native"
|
|
54
|
+
)
|
|
55
|
+
if t == "score":
|
|
56
|
+
probs = (
|
|
57
|
+
[p for p in a.get("probabilities", []) if isinstance(p, dict)]
|
|
58
|
+
if isinstance(a.get("probabilities"), list)
|
|
59
|
+
else []
|
|
60
|
+
)
|
|
61
|
+
top = probs[argmax([-1.0 if (x := num(p.get("probability"))) is None else x for p in probs])] if probs else None
|
|
62
|
+
return RawAnswer(
|
|
63
|
+
name,
|
|
64
|
+
None if top is None else top.get("label"),
|
|
65
|
+
vendor_confidence(a, sentinel),
|
|
66
|
+
a.get("score"),
|
|
67
|
+
a.get("confidence"),
|
|
68
|
+
"native",
|
|
69
|
+
)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def decode(d: Definition, tier: Tier, body: Any, *, unavailable_sentinel: bool = False) -> Decoded:
|
|
74
|
+
if not isinstance(body, dict) or not isinstance(body.get("answers"), list):
|
|
75
|
+
return nonconformant(tier, "response has no answers array")
|
|
76
|
+
names = list(d.questions.keys())
|
|
77
|
+
raws: dict[str, RawAnswer] = {}
|
|
78
|
+
for i, a in enumerate(body["answers"]):
|
|
79
|
+
if not isinstance(a, dict):
|
|
80
|
+
continue
|
|
81
|
+
name = a["name"] if isinstance(a.get("name"), str) else (names[i] if i < len(names) else None)
|
|
82
|
+
if name is None or name not in d.questions:
|
|
83
|
+
continue
|
|
84
|
+
if a.get("type") == "refusal":
|
|
85
|
+
return DecodedFail(UrimError("refused", f'{tier.id} refused question "{name}"', tier=tier.id))
|
|
86
|
+
raw = _decode_one(name, a, unavailable_sentinel)
|
|
87
|
+
if raw is not None:
|
|
88
|
+
raws[name] = raw
|
|
89
|
+
return to_answers(d, tier, raws, body)
|
urim/camps/shared.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""C: helpers shared by the camp codecs: label-space checks, the binary confidence rule, usage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from ..errors import UrimError
|
|
11
|
+
from ..types import Answer, ConfidenceSource, Definition, Question, Tier
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class RawAnswer:
|
|
16
|
+
"""A decoded native answer, already reduced to a candidate label."""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
label: Any
|
|
20
|
+
confidence: float | None
|
|
21
|
+
native_value: Any
|
|
22
|
+
native_confidence: Any
|
|
23
|
+
confidence_source: ConfidenceSource
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class TokenUsage:
|
|
28
|
+
input_tokens: int | float | None
|
|
29
|
+
output_tokens: int | float | None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class DecodedOk:
|
|
34
|
+
answers: Mapping[str, Answer]
|
|
35
|
+
usage: TokenUsage | None
|
|
36
|
+
model: str | None
|
|
37
|
+
ok: Literal[True] = True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class DecodedFail:
|
|
42
|
+
error: UrimError
|
|
43
|
+
ok: Literal[False] = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Decoded = DecodedOk | DecodedFail
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def num(v: Any) -> float | int | None:
|
|
50
|
+
if isinstance(v, bool) or not isinstance(v, (int, float)):
|
|
51
|
+
return None
|
|
52
|
+
return v if math.isfinite(v) else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def vendor_confidence(a: Mapping[str, Any], unavailable_sentinel: bool) -> float | int | None:
|
|
56
|
+
"""A vendor confidence. With the sentinel flag, 0 plus present-but-empty probabilities is "unavailable" -> None."""
|
|
57
|
+
c = num(a.get("confidence"))
|
|
58
|
+
if not unavailable_sentinel or c != 0:
|
|
59
|
+
return c
|
|
60
|
+
p = a.get("probabilities")
|
|
61
|
+
empty = isinstance(p, (list, dict)) and len(p) == 0
|
|
62
|
+
return None if empty else c
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _unit(v: Any) -> float | int | None:
|
|
66
|
+
n = num(v)
|
|
67
|
+
return n if n is not None and 0 <= n <= 1 else None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def nonconformant(tier: Tier, message: str) -> DecodedFail:
|
|
71
|
+
return DecodedFail(UrimError("nonconformant_response", message, tier=tier.id))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def binary_label(p: float) -> str:
|
|
75
|
+
"""P(yes) -> label. A tie (0.5) reads "yes"; its confidence is 0, so no threshold can pass on it."""
|
|
76
|
+
return "yes" if p >= 0.5 else "no"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def binary_confidence(p: float) -> float:
|
|
80
|
+
"""The normalized confidence (p_max - 1/n)/(1 - 1/n), n = 2, i.e. |2p - 1|, computed client-side.
|
|
81
|
+
Rounded to 12 dp exactly as urim-ts does (Math.round(x * 1e12) / 1e12; half rounds up)."""
|
|
82
|
+
x = ((max(p, 1 - p) - 0.5) / 0.5) * 1e12
|
|
83
|
+
return math.floor(x + 0.5) / 1e12
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def argmax(ps: list[float]) -> int:
|
|
87
|
+
"""Index of the largest value; first wins on a tie."""
|
|
88
|
+
best = 0
|
|
89
|
+
for i, p in enumerate(ps):
|
|
90
|
+
if p > ps[best]:
|
|
91
|
+
best = i
|
|
92
|
+
return best
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def usage_of(body: Mapping[str, Any]) -> TokenUsage | None:
|
|
96
|
+
u = body.get("usage")
|
|
97
|
+
return TokenUsage(num(u.get("input_tokens")), num(u.get("output_tokens"))) if isinstance(u, dict) else None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def to_answers(d: Definition, tier: Tier, raws: Mapping[str, RawAnswer], body: Mapping[str, Any]) -> Decoded:
|
|
101
|
+
"""Checks every question is answered inside its label space; never alters native values."""
|
|
102
|
+
answers: dict[str, Answer] = {}
|
|
103
|
+
for name, q in d.questions.items():
|
|
104
|
+
raw = raws.get(name)
|
|
105
|
+
if raw is None:
|
|
106
|
+
return nonconformant(tier, f'question "{name}" not answered')
|
|
107
|
+
if not isinstance(raw.label, str) or raw.label not in q.options:
|
|
108
|
+
return nonconformant(tier, f'question "{name}" answered outside its labels')
|
|
109
|
+
conf = _unit(raw.confidence)
|
|
110
|
+
answers[name] = Answer(
|
|
111
|
+
raw.label,
|
|
112
|
+
conf,
|
|
113
|
+
None if conf is None else raw.confidence_source,
|
|
114
|
+
confidence_formula=None
|
|
115
|
+
if conf is None
|
|
116
|
+
else ("systemone_normalized" if raw.confidence_source == "urim_derived" else "vendor_unspecified"),
|
|
117
|
+
native={"name": raw.name, "value": raw.native_value, "confidence": raw.native_confidence},
|
|
118
|
+
)
|
|
119
|
+
model = body.get("model")
|
|
120
|
+
return DecodedOk(answers, usage_of(body), model if isinstance(model, str) else None)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def score_labels(q: Question) -> list[str]:
|
|
124
|
+
return list(q.options.keys())
|
urim/camps/systemone.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""C: the systemone body (spoken by every descriptor whose camp is systemone).
|
|
2
|
+
Body shape per the systemone descriptors' vendor sources (checked 2026-10-28 EDT)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..types import Definition, Question, Tier
|
|
9
|
+
from .shared import (
|
|
10
|
+
Decoded,
|
|
11
|
+
RawAnswer,
|
|
12
|
+
argmax,
|
|
13
|
+
binary_confidence,
|
|
14
|
+
binary_label,
|
|
15
|
+
nonconformant,
|
|
16
|
+
num,
|
|
17
|
+
score_labels,
|
|
18
|
+
to_answers,
|
|
19
|
+
vendor_confidence,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
PATH = "/v1/systemone"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _criteria(q: Question) -> Any:
|
|
26
|
+
if q.type == "choice":
|
|
27
|
+
return dict(q.options)
|
|
28
|
+
if q.type == "score":
|
|
29
|
+
return list(q.options.values()) # ordered; index = level from 0
|
|
30
|
+
return {"true": q.options["yes"], "false": q.options["no"]}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def encode(d: Definition, wire_model: str, state: Any) -> Any:
|
|
34
|
+
return {
|
|
35
|
+
"model": wire_model,
|
|
36
|
+
"state": state,
|
|
37
|
+
"questions": {
|
|
38
|
+
name: {"type": q.type, "instructions": q.prompt, "criteria": _criteria(q)}
|
|
39
|
+
for name, q in d.questions.items()
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _decode_one(name: str, q: Question, a: Any, sentinel: bool) -> RawAnswer | None:
|
|
45
|
+
if not isinstance(a, dict):
|
|
46
|
+
return None
|
|
47
|
+
if q.type == "noul":
|
|
48
|
+
p = num(a.get("noul"))
|
|
49
|
+
return None if p is None else RawAnswer(name, binary_label(p), binary_confidence(p), p, None, "urim_derived")
|
|
50
|
+
if q.type == "choice":
|
|
51
|
+
return RawAnswer(
|
|
52
|
+
name, a.get("choice"), vendor_confidence(a, sentinel), a.get("choice"), a.get("confidence"), "native"
|
|
53
|
+
)
|
|
54
|
+
labels = score_labels(q)
|
|
55
|
+
given = a.get("probabilities")
|
|
56
|
+
probs: dict[str, Any] = given if isinstance(given, dict) else {}
|
|
57
|
+
ps = [num(probs.get(str(i))) for i in range(len(labels))]
|
|
58
|
+
vals = [-1.0 if p is None else p for p in ps]
|
|
59
|
+
label = None if all(v < 0 for v in vals) else labels[argmax(vals)]
|
|
60
|
+
return RawAnswer(name, label, vendor_confidence(a, sentinel), a.get("score"), a.get("confidence"), "native")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def decode(d: Definition, tier: Tier, body: Any, *, unavailable_sentinel: bool = False) -> Decoded:
|
|
64
|
+
if not isinstance(body, dict) or not isinstance(body.get("answers"), dict):
|
|
65
|
+
return nonconformant(tier, "response has no answers map")
|
|
66
|
+
raws = {}
|
|
67
|
+
for name, q in d.questions.items():
|
|
68
|
+
raw = _decode_one(name, q, body["answers"].get(name), unavailable_sentinel)
|
|
69
|
+
if raw is not None:
|
|
70
|
+
raws[name] = raw
|
|
71
|
+
return to_answers(d, tier, raws, body)
|
urim/canonical.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""C: canonical JSON (sorted keys, no whitespace) + sha256. Byte-identical to urim-ts src/canonical.ts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .jsonfmt import canonical, js_units
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def canonicalize(value: Any) -> str:
|
|
12
|
+
return canonical(value)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def sha256(text: str) -> str:
|
|
16
|
+
# Node's update(text, "utf8") writes a lone surrogate as U+FFFD; canonical output escapes them anyway.
|
|
17
|
+
clean = "".join("\ufffd" if 0xD800 <= ord(c) <= 0xDFFF else c for c in js_units(text))
|
|
18
|
+
return "sha256:" + hashlib.sha256(clean.encode("utf-8")).hexdigest()
|