modelspec-dev 0.1.0__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.
- api/__init__.py +0 -0
- api/class_fit.py +334 -0
- api/classes.py +557 -0
- api/ranking/__init__.py +12 -0
- api/ranking/engine.py +1943 -0
- cli/__init__.py +0 -0
- cli/modelspec/__init__.py +0 -0
- cli/modelspec/cli.py +1819 -0
- cli/modelspec/commands/__init__.py +0 -0
- cli/modelspec/decide_cmd.py +333 -0
- cli/modelspec/offline.py +623 -0
- cli/modelspec/snapshot.py +698 -0
- cli/modelspec/snapshot_build_cmd.py +49 -0
- cli/modelspec/verify_cmd.py +125 -0
- cli/modelspec/vocab_cmd.py +204 -0
- cli/modelspec/vocabulary_cache.py +54 -0
- decision/__init__.py +13 -0
- decision/capability.py +872 -0
- decision/computed.py +125 -0
- decision/contract.py +1575 -0
- decision/engine.py +238 -0
- decision/excluded.py +34 -0
- decision/explain.py +908 -0
- decision/filter.py +796 -0
- decision/model.py +438 -0
- decision/normalise.py +604 -0
- decision/optimise.py +320 -0
- decision/registry.py +717 -0
- decision/relax.py +132 -0
- decision/resolve.py +111 -0
- decision/schema.py +21 -0
- decision/snapshot.py +1483 -0
- decision/sources.py +544 -0
- decision/templates.py +134 -0
- decision/verify.py +1745 -0
- decision/vocabulary.py +433 -0
- modelspec_dev-0.1.0.dist-info/METADATA +101 -0
- modelspec_dev-0.1.0.dist-info/RECORD +63 -0
- modelspec_dev-0.1.0.dist-info/WHEEL +4 -0
- modelspec_dev-0.1.0.dist-info/entry_points.txt +2 -0
- modelspec_dev-0.1.0.dist-info/licenses/LICENSE +43 -0
- modelspec_dev-0.1.0.dist-info/licenses/LICENSE-DATA +428 -0
- pipeline/__init__.py +0 -0
- pipeline/class_export.py +172 -0
- pipeline/hardware.py +434 -0
- pipeline/hosts.py +247 -0
- pipeline/load.py +224 -0
- pipeline/ranking.py +551 -0
- registry/domains.yaml +130 -0
- registry/facets.yaml +888 -0
- registry/harnesses.yaml +79 -0
- registry/providers.yaml +354 -0
- registry/sources.yaml +3059 -0
- registry/templates.yaml +166 -0
- schema/__init__.py +0 -0
- schema/applicability.py +147 -0
- schema/benchmark.py +175 -0
- schema/benchmark_eligibility.py +304 -0
- schema/card.py +1463 -0
- schema/enrichment.py +162 -0
- schema/enums.py +327 -0
- schema/graph.py +406 -0
- schema/suppliers.py +72 -0
decision/relax.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""What to relax when nothing qualifies, without changing the question (MODEL-153).
|
|
2
|
+
|
|
3
|
+
A relaxation must keep the question the same question. Dropping the class
|
|
4
|
+
(`model.class = text-generator`) or a condition on a requested capability
|
|
5
|
+
domain answers a different one: recall Q10, a chat model under $0.20 per
|
|
6
|
+
million input tokens, was told to drop its class because the one model under
|
|
7
|
+
the cap was a decider. Those conditions are never suggested.
|
|
8
|
+
|
|
9
|
+
`relax` names the fewest other hard conditions whose removal gives a feasible
|
|
10
|
+
answer, preferring numeric caps and floors among equally few. `relax_to` states,
|
|
11
|
+
for each numeric cap or floor, the smallest change that admits a model: the
|
|
12
|
+
threshold moved to the nearest value an excluded candidate has.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import replace
|
|
18
|
+
from itertools import combinations
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from decision.contract import AllOf, AnyOf, Compare, NotOf, Relaxation, render_condition
|
|
22
|
+
from decision.explain import facet_unit
|
|
23
|
+
from decision.filter import apply
|
|
24
|
+
|
|
25
|
+
#: Facets that say what kind of model the question is about.
|
|
26
|
+
QUESTION_FACETS = frozenset({"model.class"})
|
|
27
|
+
#: A cap or floor, and the inclusive direction it relaxes in.
|
|
28
|
+
_LOOSENS = {"<": "<=", "<=": "<=", ">": ">=", ">=": ">="}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _leaves(cond: Any):
|
|
32
|
+
if isinstance(cond, AnyOf | AllOf):
|
|
33
|
+
for child in cond.any if isinstance(cond, AnyOf) else cond.all:
|
|
34
|
+
yield from _leaves(child)
|
|
35
|
+
elif isinstance(cond, NotOf):
|
|
36
|
+
yield from _leaves(cond.not_)
|
|
37
|
+
else:
|
|
38
|
+
yield cond
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _changes_the_question(cond: Any, domains: frozenset[str]) -> bool:
|
|
42
|
+
return any(
|
|
43
|
+
getattr(leaf, "facet", None) in QUESTION_FACETS | domains for leaf in _leaves(cond)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _numeric(value: Any) -> bool:
|
|
48
|
+
return isinstance(value, int | float) and not isinstance(value, bool)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _cap_or_floor(cond: Any) -> bool:
|
|
52
|
+
return isinstance(cond, Compare) and cond.op in _LOOSENS and _numeric(cond.value)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _relaxable(resolved, domains: frozenset[str]) -> list[int]:
|
|
56
|
+
return [
|
|
57
|
+
i for i, cond in enumerate(resolved.conditions)
|
|
58
|
+
if cond.soft is None and not _changes_the_question(cond, domains)
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def fewest(resolved, snapshot, domains: frozenset[str]) -> list[str]:
|
|
63
|
+
"""The fewest relaxable conditions whose removal gives a feasible answer."""
|
|
64
|
+
conditions = resolved.conditions
|
|
65
|
+
hard = _relaxable(resolved, domains)
|
|
66
|
+
for size in range(1, len(hard) + 1):
|
|
67
|
+
# Stable: caps and floors first, then the spec's order.
|
|
68
|
+
groups = sorted(
|
|
69
|
+
combinations(hard, size),
|
|
70
|
+
key=lambda group: -sum(_cap_or_floor(conditions[i]) for i in group),
|
|
71
|
+
)
|
|
72
|
+
for dropped in groups:
|
|
73
|
+
trial = replace(
|
|
74
|
+
resolved,
|
|
75
|
+
conditions=tuple(c for i, c in enumerate(conditions) if i not in dropped),
|
|
76
|
+
)
|
|
77
|
+
if apply(trial, snapshot).feasible:
|
|
78
|
+
return [render_condition(conditions[i]) for i in dropped]
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _unit(resolved, snapshot, facet_id: str, candidates) -> str | None:
|
|
83
|
+
if resolved.facets(facet_id).subject == "evidence":
|
|
84
|
+
for cid in candidates:
|
|
85
|
+
for row in snapshot.evidence(cid, facet_id):
|
|
86
|
+
if row.unit:
|
|
87
|
+
return row.unit
|
|
88
|
+
return None
|
|
89
|
+
return facet_unit(facet_id)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def smallest_changes(resolved, snapshot, domains: frozenset[str]) -> list[Relaxation]:
|
|
93
|
+
"""For each numeric cap or floor, the smallest change that admits a model."""
|
|
94
|
+
out: list[Relaxation] = []
|
|
95
|
+
conditions = resolved.conditions
|
|
96
|
+
for i in _relaxable(resolved, domains):
|
|
97
|
+
cond = conditions[i]
|
|
98
|
+
if not _cap_or_floor(cond):
|
|
99
|
+
continue
|
|
100
|
+
others = conditions[:i] + conditions[i + 1:]
|
|
101
|
+
# With the condition last, what it eliminates passed every other one.
|
|
102
|
+
last = apply(replace(resolved, conditions=others + (cond,)), snapshot)
|
|
103
|
+
text = render_condition(cond)
|
|
104
|
+
values = [
|
|
105
|
+
v
|
|
106
|
+
for reason in last.eliminated
|
|
107
|
+
if reason.condition == text and not reason.unverified
|
|
108
|
+
for v in (reason.value if isinstance(reason.value, tuple | list) else [reason.value])
|
|
109
|
+
if _numeric(v)
|
|
110
|
+
]
|
|
111
|
+
if not values:
|
|
112
|
+
continue
|
|
113
|
+
op = _LOOSENS[cond.op]
|
|
114
|
+
nearest = min(values) if op == "<=" else max(values)
|
|
115
|
+
if float(nearest).is_integer():
|
|
116
|
+
nearest = int(nearest) # `<= 2`, as a person would write it
|
|
117
|
+
relaxed = cond.model_copy(update={"op": op, "value": nearest})
|
|
118
|
+
admitted = apply(
|
|
119
|
+
replace(resolved, conditions=conditions[:i] + (relaxed,) + conditions[i + 1:]),
|
|
120
|
+
snapshot,
|
|
121
|
+
).feasible
|
|
122
|
+
if not admitted:
|
|
123
|
+
continue
|
|
124
|
+
out.append(Relaxation(
|
|
125
|
+
condition=text,
|
|
126
|
+
relaxed=render_condition(relaxed),
|
|
127
|
+
facet=cond.facet,
|
|
128
|
+
value=float(nearest),
|
|
129
|
+
unit=_unit(resolved, snapshot, cond.facet, admitted),
|
|
130
|
+
admits=len({snapshot.model_of(cid) for cid in admitted}),
|
|
131
|
+
))
|
|
132
|
+
return out
|
decision/resolve.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Resolve a spec before filtering (MODEL-141, design §6.1).
|
|
2
|
+
|
|
3
|
+
Validate every facet against the registry, reject a free-text ``task``, and
|
|
4
|
+
place the inventory profile's standing rules in front of ``where``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from decision.contract import (
|
|
14
|
+
AllOf,
|
|
15
|
+
AnyOf,
|
|
16
|
+
Compare,
|
|
17
|
+
EvidenceQualifiers,
|
|
18
|
+
FacetLookup,
|
|
19
|
+
InSet,
|
|
20
|
+
InventoryProfile,
|
|
21
|
+
Issue,
|
|
22
|
+
NotOf,
|
|
23
|
+
Spec,
|
|
24
|
+
SpecError,
|
|
25
|
+
check_facets,
|
|
26
|
+
)
|
|
27
|
+
from decision.registry import facet as registry_facet
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Resolved:
|
|
32
|
+
"""A spec that has passed the registry, with profile rules already in front."""
|
|
33
|
+
|
|
34
|
+
spec: Spec
|
|
35
|
+
profile: InventoryProfile | None
|
|
36
|
+
conditions: tuple[Any, ...]
|
|
37
|
+
include_retired: bool
|
|
38
|
+
facets: FacetLookup
|
|
39
|
+
objective_qualifiers: Mapping[str, EvidenceQualifiers]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_TASK_REASON = (
|
|
43
|
+
"free-text task is not yet in slice 1; send task_type and capabilities instead"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_lifecycle(facet_id: str) -> bool:
|
|
48
|
+
return facet_id == "lifecycle" or facet_id.endswith(".lifecycle")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def asks_for_retired(cond: Any) -> bool:
|
|
52
|
+
"""True when a condition admits retired models into the lineup."""
|
|
53
|
+
if isinstance(cond, AnyOf | AllOf):
|
|
54
|
+
children = cond.any if isinstance(cond, AnyOf) else cond.all
|
|
55
|
+
return any(asks_for_retired(child) for child in children)
|
|
56
|
+
if isinstance(cond, NotOf):
|
|
57
|
+
return False
|
|
58
|
+
if isinstance(cond, InSet) and cond.in_ is not None and _is_lifecycle(cond.facet):
|
|
59
|
+
return any(value == "retired" for value in cond.in_)
|
|
60
|
+
if isinstance(cond, Compare) and cond.op == "=" and _is_lifecycle(cond.facet):
|
|
61
|
+
return cond.value == "retired"
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _loaded_profile(
|
|
66
|
+
spec: Spec, profiles: Mapping[str, InventoryProfile] | None,
|
|
67
|
+
) -> InventoryProfile | None:
|
|
68
|
+
if spec.profile is None:
|
|
69
|
+
return None
|
|
70
|
+
if isinstance(spec.profile, InventoryProfile):
|
|
71
|
+
return spec.profile
|
|
72
|
+
found = None if profiles is None else profiles.get(spec.profile)
|
|
73
|
+
if not isinstance(found, InventoryProfile):
|
|
74
|
+
raise SpecError([Issue(
|
|
75
|
+
None, "profile",
|
|
76
|
+
f"profile {spec.profile} is not loaded; write it inline or pass it to resolve",
|
|
77
|
+
"profile",
|
|
78
|
+
)])
|
|
79
|
+
return found
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def resolve(
|
|
83
|
+
spec: Spec,
|
|
84
|
+
*,
|
|
85
|
+
facets: FacetLookup | None = None,
|
|
86
|
+
profiles: Mapping[str, InventoryProfile] | None = None,
|
|
87
|
+
) -> Resolved:
|
|
88
|
+
"""Validate ``spec`` and return the condition list the filter runs, rules first."""
|
|
89
|
+
lookup = facets if facets is not None else registry_facet
|
|
90
|
+
issues = check_facets(spec, lookup)
|
|
91
|
+
profile = _loaded_profile(spec, profiles)
|
|
92
|
+
if profile is not None and not isinstance(spec.profile, InventoryProfile):
|
|
93
|
+
shadow = spec.model_copy(update={"profile": profile})
|
|
94
|
+
issues.extend(
|
|
95
|
+
issue for issue in check_facets(shadow, lookup)
|
|
96
|
+
if issue.path.startswith("profile.rules")
|
|
97
|
+
)
|
|
98
|
+
if spec.task is not None:
|
|
99
|
+
issues.append(Issue(None, "task", _TASK_REASON, "task"))
|
|
100
|
+
if issues:
|
|
101
|
+
raise SpecError(issues)
|
|
102
|
+
rules = () if profile is None else tuple(profile.rules)
|
|
103
|
+
conditions = rules + tuple(spec.where)
|
|
104
|
+
return Resolved(
|
|
105
|
+
spec=spec,
|
|
106
|
+
profile=profile,
|
|
107
|
+
conditions=conditions,
|
|
108
|
+
include_retired=any(asks_for_retired(cond) for cond in conditions),
|
|
109
|
+
facets=lookup,
|
|
110
|
+
objective_qualifiers=spec.optimize.qualifiers,
|
|
111
|
+
)
|
decision/schema.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Write docs/decision-contract.schema.json from the contract types.
|
|
2
|
+
|
|
3
|
+
python -m decision.schema
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from decision.contract import render_json_schema
|
|
11
|
+
|
|
12
|
+
SCHEMA_PATH = Path(__file__).resolve().parent.parent / "docs" / "decision-contract.schema.json"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
SCHEMA_PATH.write_text(render_json_schema())
|
|
17
|
+
print(f"wrote {SCHEMA_PATH}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
main()
|