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
schema/graph.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""FalkorDB graph schema — node types, edge types, and Cypher operations.
|
|
2
|
+
|
|
3
|
+
This module defines the complete ontology and provides functions to:
|
|
4
|
+
- Create indexes
|
|
5
|
+
- Ingest a ModelCard into the graph as nodes + edges
|
|
6
|
+
- Run common traversal queries
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .enums import DeviceClass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ═══════════════════════════════════════════════════════════════
|
|
20
|
+
# Edge type registry
|
|
21
|
+
# ═══════════════════════════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
class EdgeType(str, Enum):
|
|
24
|
+
"""All relationship types in the graph."""
|
|
25
|
+
# Factual (directly observed)
|
|
26
|
+
MADE_BY = "MADE_BY"
|
|
27
|
+
DERIVED_FROM = "DERIVED_FROM"
|
|
28
|
+
MERGED_FROM = "MERGED_FROM"
|
|
29
|
+
LICENSED_AS = "LICENSED_AS"
|
|
30
|
+
HAS_CAPABILITY = "HAS_CAPABILITY"
|
|
31
|
+
SCORED_ON = "SCORED_ON"
|
|
32
|
+
FITS_ON = "FITS_ON"
|
|
33
|
+
AVAILABLE_AS = "AVAILABLE_AS"
|
|
34
|
+
RUNS_ON = "RUNS_ON"
|
|
35
|
+
AVAILABLE_ON = "AVAILABLE_ON"
|
|
36
|
+
TAGGED_WITH = "TAGGED_WITH"
|
|
37
|
+
SUITED_FOR = "SUITED_FOR"
|
|
38
|
+
PUBLISHED_BY = "PUBLISHED_BY"
|
|
39
|
+
SUPPORTS_RUNTIME = "SUPPORTS_RUNTIME"
|
|
40
|
+
TESTS_CAPABILITY = "TESTS_CAPABILITY"
|
|
41
|
+
REQUIRED_BY = "REQUIRED_BY"
|
|
42
|
+
HOSTS = "HOSTS" # (:Host)-[:HOSTS]->(:Hardware), unified hosts only
|
|
43
|
+
|
|
44
|
+
# Derived (computed by ranking engine)
|
|
45
|
+
COMPETES_WITH = "COMPETES_WITH"
|
|
46
|
+
OUTPERFORMS = "OUTPERFORMS"
|
|
47
|
+
SIMILAR_TO = "SIMILAR_TO"
|
|
48
|
+
UPGRADE_PATH = "UPGRADE_PATH"
|
|
49
|
+
BEST_FOR = "BEST_FOR"
|
|
50
|
+
PARETO_OPTIMAL = "PARETO_OPTIMAL"
|
|
51
|
+
|
|
52
|
+
# Institutional (organization-specific overlay)
|
|
53
|
+
APPROVED_BY = "APPROVED_BY"
|
|
54
|
+
EXCLUDED_BY = "EXCLUDED_BY"
|
|
55
|
+
REVIEWED_BY = "REVIEWED_BY"
|
|
56
|
+
PREFERRED_BY = "PREFERRED_BY"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ═══════════════════════════════════════════════════════════════
|
|
60
|
+
# Index definitions
|
|
61
|
+
# ═══════════════════════════════════════════════════════════════
|
|
62
|
+
|
|
63
|
+
INDEXES = [
|
|
64
|
+
"CREATE INDEX ON :Model(id)",
|
|
65
|
+
"CREATE INDEX ON :Model(model_type)",
|
|
66
|
+
"CREATE INDEX ON :Model(status)",
|
|
67
|
+
"CREATE INDEX ON :Model(origin_country)",
|
|
68
|
+
"CREATE INDEX ON :Model(open_weights)",
|
|
69
|
+
"CREATE INDEX ON :Model(total_parameters)",
|
|
70
|
+
"CREATE INDEX ON :Model(arena_elo_overall)",
|
|
71
|
+
"CREATE INDEX ON :Model(cost_input)",
|
|
72
|
+
"CREATE INDEX ON :Model(context_window)",
|
|
73
|
+
"CREATE INDEX ON :Model(release_date)",
|
|
74
|
+
"CREATE INDEX ON :Provider(id)",
|
|
75
|
+
"CREATE INDEX ON :Platform(id)",
|
|
76
|
+
"CREATE INDEX ON :Platform(category)",
|
|
77
|
+
"CREATE INDEX ON :Capability(id)",
|
|
78
|
+
"CREATE INDEX ON :Capability(category)",
|
|
79
|
+
"CREATE INDEX ON :Hardware(id)",
|
|
80
|
+
"CREATE INDEX ON :Hardware(memory_gb)",
|
|
81
|
+
# "which models fit datacentre hardware" is a class filter before it is a
|
|
82
|
+
# traversal, so the class is indexed like any other node grouping key.
|
|
83
|
+
"CREATE INDEX ON :Hardware(device_class)",
|
|
84
|
+
"CREATE INDEX ON :Host(id)",
|
|
85
|
+
"CREATE INDEX ON :Benchmark(id)",
|
|
86
|
+
"CREATE INDEX ON :Benchmark(category)",
|
|
87
|
+
"CREATE INDEX ON :License(id)",
|
|
88
|
+
"CREATE INDEX ON :UseCase(id)",
|
|
89
|
+
"CREATE INDEX ON :DownselectProfile(id)",
|
|
90
|
+
"CREATE INDEX ON :Runtime(id)",
|
|
91
|
+
"CREATE INDEX ON :Quantization(id)",
|
|
92
|
+
"CREATE INDEX ON :Tag(id)",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def create_indexes(graph) -> None:
|
|
97
|
+
"""Create all indexes on a FalkorDB graph instance."""
|
|
98
|
+
for idx in INDEXES:
|
|
99
|
+
try:
|
|
100
|
+
graph.query(idx)
|
|
101
|
+
except Exception:
|
|
102
|
+
pass # Index may already exist
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ═══════════════════════════════════════════════════════════════
|
|
106
|
+
# Ingestion: ModelCard → Graph nodes + edges
|
|
107
|
+
# ═══════════════════════════════════════════════════════════════
|
|
108
|
+
|
|
109
|
+
def device_class_of(hw_id: str, device_classes: Mapping[str, str] | None) -> str | None:
|
|
110
|
+
"""The class of the device `hw_id` names, or None when nothing knows it.
|
|
111
|
+
|
|
112
|
+
`hardware/*.yaml` is the only source of a device class, and this module is
|
|
113
|
+
below the loader that reads it, so the caller passes the mapping in. A card
|
|
114
|
+
may key a hardware profile on anything, including the four legacy ids the
|
|
115
|
+
card schema still defaults to (`nvidia_5090_32gb` and friends), which name
|
|
116
|
+
no device record. Those get no class rather than a guessed one: a Hardware
|
|
117
|
+
node wrongly labelled `datacentre` is worse than one a class filter skips.
|
|
118
|
+
|
|
119
|
+
A value outside `DeviceClass` is a typo in a record, not a new class, and
|
|
120
|
+
is refused here so it cannot reach the export and split a group in two.
|
|
121
|
+
"""
|
|
122
|
+
if not device_classes:
|
|
123
|
+
return None
|
|
124
|
+
value = device_classes.get(hw_id)
|
|
125
|
+
if value is None:
|
|
126
|
+
return None
|
|
127
|
+
return DeviceClass(value).value
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def ingest_model_card(graph, card, *,
|
|
131
|
+
device_classes: Mapping[str, str] | None = None) -> dict[str, int]:
|
|
132
|
+
"""Ingest a ModelCard into FalkorDB, creating/merging all nodes and edges.
|
|
133
|
+
|
|
134
|
+
`device_classes` maps a device id to its `DeviceClass` value (from
|
|
135
|
+
`pipeline.hardware.device_classes`). It is optional so the FalkorDB ingest
|
|
136
|
+
and the ranking CLI keep working without the hardware records; when it is
|
|
137
|
+
given, the Hardware nodes this derives carry `device_class` and can be
|
|
138
|
+
grouped by it.
|
|
139
|
+
|
|
140
|
+
Returns a dict with counts: {"nodes_created": N, "edges_created": M}
|
|
141
|
+
"""
|
|
142
|
+
from .card import ModelCard
|
|
143
|
+
|
|
144
|
+
# Accept either a sink or a raw FalkorDB handle, so existing callers such
|
|
145
|
+
# as scripts/ingest_all.py keep working unchanged.
|
|
146
|
+
sink = graph if hasattr(graph, "node") else CypherSink(graph)
|
|
147
|
+
|
|
148
|
+
stats = {"nodes_created": 0, "edges_created": 0}
|
|
149
|
+
ident = card.identity
|
|
150
|
+
arch = card.architecture
|
|
151
|
+
lic = card.licensing
|
|
152
|
+
|
|
153
|
+
# ── 1. Upsert the :Model node ──────────────────────────
|
|
154
|
+
model_props = {
|
|
155
|
+
"id": ident.model_id,
|
|
156
|
+
"display_name": ident.display_name,
|
|
157
|
+
"model_type": ident.model_type.value if ident.model_type else None,
|
|
158
|
+
"status": ident.status.value,
|
|
159
|
+
"release_date": ident.release_date,
|
|
160
|
+
"family": ident.family,
|
|
161
|
+
"origin_country": lic.origin_country,
|
|
162
|
+
"open_weights": lic.open_weights,
|
|
163
|
+
"total_parameters": arch.total_parameters,
|
|
164
|
+
"active_parameters": arch.active_parameters,
|
|
165
|
+
"architecture_type": arch.type.value if arch.type else None,
|
|
166
|
+
"context_window": card.modalities.text.context_window,
|
|
167
|
+
"max_input": card.modalities.text.max_input_tokens,
|
|
168
|
+
"max_output": card.modalities.text.max_output_tokens,
|
|
169
|
+
"cost_input": card.cost.input,
|
|
170
|
+
"cost_output": card.cost.output,
|
|
171
|
+
"arena_elo_overall": card.benchmarks.scores.get("arena_elo_overall"),
|
|
172
|
+
"custom_score": card.downselect.custom_score,
|
|
173
|
+
"embedding_dimensions": arch.embedding_dimensions,
|
|
174
|
+
"reasoning": card.capabilities.reasoning.chain_of_thought,
|
|
175
|
+
"tool_call": card.capabilities.tool_use.function_calling,
|
|
176
|
+
"vision_input": card.modalities.vision.supported,
|
|
177
|
+
"multilingual": card.capabilities.language.multilingual,
|
|
178
|
+
"model_subtypes": ",".join(st.value for st in ident.model_subtypes) if ident.model_subtypes else None,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# Filter out None values for cleaner Cypher
|
|
182
|
+
props = {k: v for k, v in model_props.items() if v is not None}
|
|
183
|
+
sink.node("Model", "id", ident.model_id, props)
|
|
184
|
+
stats["nodes_created"] += 1
|
|
185
|
+
|
|
186
|
+
# ── 2. Upsert :Provider and :MADE_BY ───────────────────
|
|
187
|
+
if ident.provider:
|
|
188
|
+
sink.node("Provider", "id", ident.provider, {
|
|
189
|
+
"id": ident.provider,
|
|
190
|
+
"display_name": ident.provider_display or ident.provider,
|
|
191
|
+
"country": lic.origin_country,
|
|
192
|
+
})
|
|
193
|
+
sink.edge("Model", ident.model_id, "MADE_BY", "Provider", ident.provider)
|
|
194
|
+
stats["nodes_created"] += 1
|
|
195
|
+
stats["edges_created"] += 1
|
|
196
|
+
|
|
197
|
+
# ── 3. Upsert :License and :LICENSED_AS ────────────────
|
|
198
|
+
if lic.license_type:
|
|
199
|
+
license_id = lic.license_type.value
|
|
200
|
+
sink.node("License", "id", license_id, {
|
|
201
|
+
"id": license_id,
|
|
202
|
+
"name": license_id,
|
|
203
|
+
# A UsePermission since MODEL-77, not a bool: "restricted"
|
|
204
|
+
# is the answer for 169 cards and had nowhere to go before.
|
|
205
|
+
"commercial_ok": lic.commercial_use.value,
|
|
206
|
+
"defense_ok": lic.defense_use.value,
|
|
207
|
+
"government_ok": lic.government_use.value,
|
|
208
|
+
})
|
|
209
|
+
sink.edge("Model", ident.model_id, "LICENSED_AS", "License", license_id)
|
|
210
|
+
stats["edges_created"] += 1
|
|
211
|
+
|
|
212
|
+
# ── 4. Upsert :DERIVED_FROM (lineage) ──────────────────
|
|
213
|
+
if card.lineage.base_model:
|
|
214
|
+
sink.node("Model", "id", card.lineage.base_model, {
|
|
215
|
+
"id": card.lineage.base_model,
|
|
216
|
+
})
|
|
217
|
+
edge_props = {}
|
|
218
|
+
if card.lineage.base_model_relation:
|
|
219
|
+
edge_props["relation"] = card.lineage.base_model_relation.value
|
|
220
|
+
sink.edge("Model", ident.model_id, "DERIVED_FROM",
|
|
221
|
+
"Model", card.lineage.base_model, edge_props)
|
|
222
|
+
stats["edges_created"] += 1
|
|
223
|
+
|
|
224
|
+
# ── 5. Capabilities → :HAS_CAPABILITY edges ───────────
|
|
225
|
+
cap_map = _extract_capabilities(card.capabilities)
|
|
226
|
+
for cap_id, tier in cap_map.items():
|
|
227
|
+
category = cap_id.split(":")[0] if ":" in cap_id else "general"
|
|
228
|
+
sink.node("Capability", "id", cap_id, {
|
|
229
|
+
"id": cap_id,
|
|
230
|
+
"category": category,
|
|
231
|
+
"name": cap_id.split(":")[-1].replace("_", " ").title(),
|
|
232
|
+
})
|
|
233
|
+
sink.edge("Model", ident.model_id, "HAS_CAPABILITY",
|
|
234
|
+
"Capability", cap_id, {"tier": tier})
|
|
235
|
+
stats["edges_created"] += 1
|
|
236
|
+
|
|
237
|
+
# ── 6. Benchmarks → :SCORED_ON edges ───────────────────
|
|
238
|
+
for bench_id, value in card.benchmarks.scores.items():
|
|
239
|
+
if isinstance(value, (int, float)):
|
|
240
|
+
sink.node("Benchmark", "id", bench_id, {
|
|
241
|
+
"id": bench_id,
|
|
242
|
+
"name": bench_id.replace("_", " ").title(),
|
|
243
|
+
})
|
|
244
|
+
sink.edge("Model", ident.model_id, "SCORED_ON",
|
|
245
|
+
"Benchmark", bench_id, {
|
|
246
|
+
"value": float(value),
|
|
247
|
+
"date": card.benchmarks.benchmark_as_of,
|
|
248
|
+
})
|
|
249
|
+
stats["edges_created"] += 1
|
|
250
|
+
|
|
251
|
+
# ── 7. Hardware profiles → :FITS_ON edges ──────────────
|
|
252
|
+
for hw_id, profile in card.deployment.hardware_profiles.items():
|
|
253
|
+
if profile.fits:
|
|
254
|
+
hw_props = {"id": hw_id, "display_name": hw_id}
|
|
255
|
+
hw_class = device_class_of(hw_id, device_classes)
|
|
256
|
+
if hw_class is not None:
|
|
257
|
+
hw_props["device_class"] = hw_class
|
|
258
|
+
sink.node("Hardware", "id", hw_id, hw_props)
|
|
259
|
+
edge_props = {k: v for k, v in {
|
|
260
|
+
"quantization": profile.best_quant,
|
|
261
|
+
"vram_usage_gb": profile.vram_usage_gb or profile.ram_usage_gb,
|
|
262
|
+
"tokens_per_sec": profile.tokens_per_sec,
|
|
263
|
+
"ttft_ms": profile.ttft_ms,
|
|
264
|
+
"max_context_tokens": profile.max_context_at_quant,
|
|
265
|
+
"inference_engine": profile.inference_engine,
|
|
266
|
+
}.items() if v}
|
|
267
|
+
sink.edge("Model", ident.model_id, "FITS_ON",
|
|
268
|
+
"Hardware", hw_id, edge_props)
|
|
269
|
+
stats["edges_created"] += 1
|
|
270
|
+
|
|
271
|
+
# ── 8. Platform availability → :AVAILABLE_ON edges ─────
|
|
272
|
+
for field_name, field_value in card.availability:
|
|
273
|
+
if isinstance(field_value, PlatformEntry_type()) and field_value.available:
|
|
274
|
+
platform_id = field_name
|
|
275
|
+
sink.node("Platform", "id", platform_id, {
|
|
276
|
+
"id": platform_id,
|
|
277
|
+
"display_name": field_name.replace("_", " ").title(),
|
|
278
|
+
"url": field_value.url,
|
|
279
|
+
})
|
|
280
|
+
edge_props = {k: v for k, v in {
|
|
281
|
+
"model_id_on_platform": field_value.model_id,
|
|
282
|
+
"fine_tuning": field_value.fine_tuning,
|
|
283
|
+
"gated": field_value.gated,
|
|
284
|
+
"notes": field_value.notes,
|
|
285
|
+
}.items() if v}
|
|
286
|
+
sink.edge("Model", ident.model_id, "AVAILABLE_ON",
|
|
287
|
+
"Platform", platform_id, edge_props)
|
|
288
|
+
stats["edges_created"] += 1
|
|
289
|
+
|
|
290
|
+
# ── 9. Tags → :TAGGED_WITH edges ──────────────────────
|
|
291
|
+
for tag in ident.tags:
|
|
292
|
+
sink.node("Tag", "id", tag, {"id": tag})
|
|
293
|
+
sink.edge("Model", ident.model_id, "TAGGED_WITH", "Tag", tag)
|
|
294
|
+
stats["edges_created"] += 1
|
|
295
|
+
|
|
296
|
+
return stats
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def PlatformEntry_type():
|
|
300
|
+
"""Lazy import to avoid circular dependency."""
|
|
301
|
+
from .card import PlatformEntry
|
|
302
|
+
return PlatformEntry
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ═══════════════════════════════════════════════════════════════
|
|
306
|
+
# Helpers
|
|
307
|
+
# ═══════════════════════════════════════════════════════════════
|
|
308
|
+
|
|
309
|
+
class CypherSink:
|
|
310
|
+
"""Writes the derived graph into FalkorDB, one MERGE at a time."""
|
|
311
|
+
|
|
312
|
+
def __init__(self, graph) -> None:
|
|
313
|
+
self.graph = graph
|
|
314
|
+
|
|
315
|
+
def node(self, label: str, key_field: str, key_value: str, props: dict[str, Any]) -> None:
|
|
316
|
+
extra_props = {k: v for k, v in props.items() if k != key_field}
|
|
317
|
+
if extra_props:
|
|
318
|
+
prop_str = ", ".join(f"n.{k} = ${k}" for k in extra_props)
|
|
319
|
+
query = f"MERGE (n:{label} {{{key_field}: ${key_field}}}) SET {prop_str}"
|
|
320
|
+
else:
|
|
321
|
+
query = f"MERGE (n:{label} {{{key_field}: ${key_field}}})"
|
|
322
|
+
self.graph.query(query, props)
|
|
323
|
+
|
|
324
|
+
def edge(self, from_label: str, from_id: str, edge_type: str,
|
|
325
|
+
to_label: str, to_id: str, props: dict[str, Any] | None = None) -> None:
|
|
326
|
+
if props:
|
|
327
|
+
prop_str = " {" + ", ".join(f"{k}: ${k}" for k in props) + "}"
|
|
328
|
+
else:
|
|
329
|
+
prop_str = ""
|
|
330
|
+
params = {"from_id": from_id, "to_id": to_id, **(props or {})}
|
|
331
|
+
query = (
|
|
332
|
+
f"MATCH (a:{from_label} {{id: $from_id}}) "
|
|
333
|
+
f"MATCH (b:{to_label} {{id: $to_id}}) "
|
|
334
|
+
f"MERGE (a)-[:{edge_type}{prop_str}]->(b)"
|
|
335
|
+
)
|
|
336
|
+
self.graph.query(query, params)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class CollectingSink:
|
|
340
|
+
"""Accumulates the derived graph in memory, with no database.
|
|
341
|
+
|
|
342
|
+
This is what lets the build emit the graph as JSON. It reproduces MERGE
|
|
343
|
+
semantics: a node seen twice keeps the union of its properties, later
|
|
344
|
+
non-null values winning, so a model referenced as another model's base
|
|
345
|
+
before its own card is read still ends up complete.
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
def __init__(self) -> None:
|
|
349
|
+
self.nodes: dict[tuple[str, str], dict[str, Any]] = {}
|
|
350
|
+
self.edges: list[dict[str, Any]] = []
|
|
351
|
+
|
|
352
|
+
def node(self, label: str, key_field: str, key_value: str, props: dict[str, Any]) -> None:
|
|
353
|
+
existing = self.nodes.setdefault((label, key_value), {key_field: key_value})
|
|
354
|
+
for key, value in props.items():
|
|
355
|
+
if value is not None:
|
|
356
|
+
existing[key] = value
|
|
357
|
+
|
|
358
|
+
def edge(self, from_label: str, from_id: str, edge_type: str,
|
|
359
|
+
to_label: str, to_id: str, props: dict[str, Any] | None = None) -> None:
|
|
360
|
+
self.edges.append({
|
|
361
|
+
"type": edge_type,
|
|
362
|
+
"from": from_id,
|
|
363
|
+
"from_label": from_label,
|
|
364
|
+
"to": to_id,
|
|
365
|
+
"to_label": to_label,
|
|
366
|
+
**({"props": props} if props else {}),
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def derive_graph(cards, device_classes: Mapping[str, str] | None = None) -> CollectingSink:
|
|
371
|
+
"""Derive the whole graph from model cards, without touching a database.
|
|
372
|
+
|
|
373
|
+
`device_classes` is passed straight to `ingest_model_card`; see there.
|
|
374
|
+
"""
|
|
375
|
+
sink = CollectingSink()
|
|
376
|
+
for card in cards:
|
|
377
|
+
ingest_model_card(sink, card, device_classes=device_classes)
|
|
378
|
+
return sink
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _extract_capabilities(caps) -> dict[str, str]:
|
|
382
|
+
"""Extract capability IDs and tiers from the Capabilities model."""
|
|
383
|
+
result = {}
|
|
384
|
+
|
|
385
|
+
def _process_section(prefix: str, section):
|
|
386
|
+
for field_name, _ in section:
|
|
387
|
+
value = getattr(section, field_name)
|
|
388
|
+
if field_name == "overall" and value:
|
|
389
|
+
result[prefix] = value.value if hasattr(value, "value") else str(value)
|
|
390
|
+
elif isinstance(value, bool) and value:
|
|
391
|
+
cap_id = f"{prefix}:{field_name}"
|
|
392
|
+
# Boolean capabilities get tier from their section's overall
|
|
393
|
+
overall = getattr(section, "overall", None)
|
|
394
|
+
tier = overall.value if overall and hasattr(overall, "value") else "tier-2"
|
|
395
|
+
result[cap_id] = tier
|
|
396
|
+
|
|
397
|
+
_process_section("coding", caps.coding)
|
|
398
|
+
_process_section("reasoning", caps.reasoning)
|
|
399
|
+
_process_section("tool_use", caps.tool_use)
|
|
400
|
+
_process_section("language", caps.language)
|
|
401
|
+
_process_section("creative", caps.creative)
|
|
402
|
+
_process_section("safety", caps.safety_alignment)
|
|
403
|
+
_process_section("domain", caps.domain_specific)
|
|
404
|
+
_process_section("agent", caps.agent_capabilities)
|
|
405
|
+
|
|
406
|
+
return result
|
schema/suppliers.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Organisations ModelSpec buys from, and what a purchase costs us (MODEL-101).
|
|
2
|
+
|
|
3
|
+
ModelSpec pays TypeSafe for API access and runs **Jev — TypeSafe's own model —**
|
|
4
|
+
over ambiguous creator attributions (`scripts/attribution.py`). Then the
|
|
5
|
+
catalogue acquired a TypeSafe card, so the supplier is now also a subject.
|
|
6
|
+
|
|
7
|
+
Money flows *to* the vendor, so nothing in `neutrality_commitment()`
|
|
8
|
+
(`api/ranking/engine.py`) is contradicted: no referral fee, no paid placement,
|
|
9
|
+
no provider-paid visibility, and a purchase buys a supplier nothing in a
|
|
10
|
+
ranking. Two things follow anyway, and neither is a promise — both are
|
|
11
|
+
mechanisms:
|
|
12
|
+
|
|
13
|
+
1. **The reader is told.** A card whose provider is named here carries the
|
|
14
|
+
disclosure on its page, derived from this table rather than authored per
|
|
15
|
+
card, so it cannot be left off the next one (`pipeline/render.py`).
|
|
16
|
+
2. **A supplier's model never writes a field on that supplier's card.**
|
|
17
|
+
`scripts/attribution.py` refuses the judgment before it is asked, and
|
|
18
|
+
refuses a stored one before it is applied. `tests/test_attribution.py`
|
|
19
|
+
fails if either refusal is removed.
|
|
20
|
+
|
|
21
|
+
What is *not* refused: deterministic attribution. An id prefix that names
|
|
22
|
+
TypeSafe is code reading a string — no model call and no opinion — and it stays
|
|
23
|
+
allowed. The rule is about the judgment, and the judgment alone.
|
|
24
|
+
|
|
25
|
+
One table, two readers, so the org the page discloses and the org the guard
|
|
26
|
+
protects cannot drift apart.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Supplier:
|
|
36
|
+
"""A vendor ModelSpec pays, that the catalogue also documents."""
|
|
37
|
+
|
|
38
|
+
#: The catalogue's provider slug: `models/<slug>/`.
|
|
39
|
+
slug: str
|
|
40
|
+
display: str
|
|
41
|
+
#: What we buy. Shown to a reader, so it is a sentence, not a label.
|
|
42
|
+
relationship: str
|
|
43
|
+
#: The rule that follows from it, in the same words on the page and here.
|
|
44
|
+
rule: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
SUPPLIERS: dict[str, Supplier] = {
|
|
48
|
+
"typesafe": Supplier(
|
|
49
|
+
slug="typesafe",
|
|
50
|
+
display="TypeSafe AI",
|
|
51
|
+
relationship=(
|
|
52
|
+
"ModelSpec is a paying TypeSafe customer. The catalogue's own pipeline sends "
|
|
53
|
+
"ambiguous creator attributions to Jev and pays per input token for the answer "
|
|
54
|
+
"(scripts/attribution.py, MODEL-82)."
|
|
55
|
+
),
|
|
56
|
+
rule=(
|
|
57
|
+
"No field on a TypeSafe card may be written by a Jev judgment. TypeSafe listings "
|
|
58
|
+
"are attributed by code or by a person, or they are left unattributed; the "
|
|
59
|
+
"judgment is refused before it is asked."
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#: Provider slugs under the rule. The guard and the page read the same set.
|
|
65
|
+
SUPPLIER_SLUGS: frozenset[str] = frozenset(SUPPLIERS)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def supplier_for(provider: str | None) -> Supplier | None:
|
|
69
|
+
"""The supplier a card's `provider` names, or `None` for every other card."""
|
|
70
|
+
if not provider:
|
|
71
|
+
return None
|
|
72
|
+
return SUPPLIERS.get(str(provider).strip().lower())
|