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.
Files changed (63) hide show
  1. api/__init__.py +0 -0
  2. api/class_fit.py +334 -0
  3. api/classes.py +557 -0
  4. api/ranking/__init__.py +12 -0
  5. api/ranking/engine.py +1943 -0
  6. cli/__init__.py +0 -0
  7. cli/modelspec/__init__.py +0 -0
  8. cli/modelspec/cli.py +1819 -0
  9. cli/modelspec/commands/__init__.py +0 -0
  10. cli/modelspec/decide_cmd.py +333 -0
  11. cli/modelspec/offline.py +623 -0
  12. cli/modelspec/snapshot.py +698 -0
  13. cli/modelspec/snapshot_build_cmd.py +49 -0
  14. cli/modelspec/verify_cmd.py +125 -0
  15. cli/modelspec/vocab_cmd.py +204 -0
  16. cli/modelspec/vocabulary_cache.py +54 -0
  17. decision/__init__.py +13 -0
  18. decision/capability.py +872 -0
  19. decision/computed.py +125 -0
  20. decision/contract.py +1575 -0
  21. decision/engine.py +238 -0
  22. decision/excluded.py +34 -0
  23. decision/explain.py +908 -0
  24. decision/filter.py +796 -0
  25. decision/model.py +438 -0
  26. decision/normalise.py +604 -0
  27. decision/optimise.py +320 -0
  28. decision/registry.py +717 -0
  29. decision/relax.py +132 -0
  30. decision/resolve.py +111 -0
  31. decision/schema.py +21 -0
  32. decision/snapshot.py +1483 -0
  33. decision/sources.py +544 -0
  34. decision/templates.py +134 -0
  35. decision/verify.py +1745 -0
  36. decision/vocabulary.py +433 -0
  37. modelspec_dev-0.1.0.dist-info/METADATA +101 -0
  38. modelspec_dev-0.1.0.dist-info/RECORD +63 -0
  39. modelspec_dev-0.1.0.dist-info/WHEEL +4 -0
  40. modelspec_dev-0.1.0.dist-info/entry_points.txt +2 -0
  41. modelspec_dev-0.1.0.dist-info/licenses/LICENSE +43 -0
  42. modelspec_dev-0.1.0.dist-info/licenses/LICENSE-DATA +428 -0
  43. pipeline/__init__.py +0 -0
  44. pipeline/class_export.py +172 -0
  45. pipeline/hardware.py +434 -0
  46. pipeline/hosts.py +247 -0
  47. pipeline/load.py +224 -0
  48. pipeline/ranking.py +551 -0
  49. registry/domains.yaml +130 -0
  50. registry/facets.yaml +888 -0
  51. registry/harnesses.yaml +79 -0
  52. registry/providers.yaml +354 -0
  53. registry/sources.yaml +3059 -0
  54. registry/templates.yaml +166 -0
  55. schema/__init__.py +0 -0
  56. schema/applicability.py +147 -0
  57. schema/benchmark.py +175 -0
  58. schema/benchmark_eligibility.py +304 -0
  59. schema/card.py +1463 -0
  60. schema/enrichment.py +162 -0
  61. schema/enums.py +327 -0
  62. schema/graph.py +406 -0
  63. 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()