structured-data-models-client 1.0.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.
sdm_client/models.py ADDED
@@ -0,0 +1,251 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES.
2
+ # All rights reserved.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ r"""The models this client can send to.
6
+
7
+ Pass one as an argument rather than calling a differently-named method:
8
+
9
+ >>> client.predict(request, model=KumoTabular())
10
+ >>> client.predict(request, model=KumoRelational())
11
+
12
+ Each is a thin handle over a :class:`~sdm_client.base.ModelSpec`. A handle
13
+ carries no state and no configuration. Gathering knobs live on the request and
14
+ output fields on the call, so constructing one is free and two of the same
15
+ kind are interchangeable.
16
+
17
+ The declarations below mirror what deployed endpoints actually answer,
18
+ rather than what any design document says they should.
19
+
20
+ **A runtime contract describes a deployment, and one model id may have more
21
+ than one.** The relational declaration below is the broader of those in
22
+ service: six task kinds and all seven output fields. A narrower build of the
23
+ same id serves only classification and regression, and produces only
24
+ ``prediction``, ``probabilities`` and ``quantiles``. Asking that one for a
25
+ forecast passes the checks here and is refused by the endpoint.
26
+
27
+ Reading this from the endpoint instead would settle it, but the Universal TFM
28
+ contract has no capabilities route and ``/v1/models`` returns an id and nothing
29
+ else. So this is the broader of the two on purpose: refusing locally what one
30
+ deployment can answer would be worse than letting the other say no.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ from predictive_query import RELATIONAL, TABULAR
36
+ from sdm_api import TFM_MODEL_KUMO_RELATIONAL, TFM_MODEL_KUMO_TABULAR
37
+
38
+ from sdm_client.base import ModelSpec, RuntimeContract
39
+ from sdm_client.errors import StructuredDataError, UnknownModelError
40
+
41
+ # Sizes the relational endpoint enforces. `predict_rows_max` is the serving
42
+ # layer's own `TFM_MAX_TEST_ROWS`, measured at the boundary rather than read
43
+ # from a document: 10,000 rows to score are answered, and 10,001 come back
44
+ #
45
+ # [422 UNSUPPORTED_SHAPE] N_test 10001 exceeds limit 10000.
46
+ #
47
+ # A deployment may be configured to hold to less, which is what the
48
+ # `rows_per_call` option on materialize() is for.
49
+ #
50
+ # The endpoint refuses past these, so the client refuses first with a message
51
+ # that names the limit. See the architecture note on inference knobs:
52
+ # `run_mode` is derived from these rather than being set.
53
+ #
54
+ # `predict_rows_max` and `predict_rows_temporal_link_prediction` restate the
55
+ # engine's own `_MAX_PRED_SIZE`, which defaults to the first and holds the
56
+ # second for `TaskType.TEMPORAL_LINK_PREDICTION`
57
+ # (sdm_relational_engine/rfm/rfm.py). The duplication predates this client,
58
+ # and it is load-bearing: this value is what the client asks the engine to
59
+ # batch at, so a drift would gather batches the endpoint refuses, or ask for
60
+ # smaller ones than it would take, and the engine's own refusal names a
61
+ # `batch_size=` argument this client's callers have not got. Change them
62
+ # together; `test_the_two_ceilings_are_the_engine_s_own` fails if they move
63
+ # apart.
64
+ RELATIONAL_LIMITS = {
65
+ 'context_rows_fast': 1_000,
66
+ 'context_rows_max': 10_000,
67
+ 'predict_rows_max': 10_000,
68
+ 'predict_rows_temporal_link_prediction': 200,
69
+ 'explanation_predict_rows': 1,
70
+ 'hops_max': 6,
71
+ 'tables_max': 15,
72
+ 'classes_max': 10,
73
+ }
74
+
75
+ # A relational regression is answered from a predicted distribution, and the
76
+ # endpoint reports its mean unless told otherwise; the engine reports the
77
+ # median, so that is asked for by default.
78
+ DEFAULT_PREDICTION_STATISTIC = 'median'
79
+ PREDICTION_STATISTICS = frozenset({'mean', 'median'})
80
+ SUMMARISED_TASK_KINDS = frozenset({'regression', 'forecasting'})
81
+
82
+ # The tabular endpoint refuses a context past this, exactly:
83
+ #
84
+ # [422 UNSUPPORTED_SHAPE] N_train 10001 exceeds limit 10000.
85
+ #
86
+ # Measured against the deployment rather than read from a document, at the
87
+ # boundary: 10,000 rows answer, 10,001 refuse. Declared so the client refuses
88
+ # first, because the alternative is encoding and transmitting an arbitrarily
89
+ # large context to be told the limit at the far end.
90
+ TABULAR_LIMITS = {
91
+ 'context_rows_max': 10_000,
92
+ }
93
+
94
+ KUMO_TABULAR = ModelSpec(
95
+ model_id=TFM_MODEL_KUMO_TABULAR,
96
+ profile=TABULAR,
97
+ runtime=RuntimeContract(
98
+ # The endpoint takes the coarse kind only and decides binary versus
99
+ # multiclass itself, so this client never narrows it here.
100
+ tasks=frozenset({'classification', 'regression'}),
101
+ outputs=frozenset({'prediction', 'probabilities', 'quantiles'}),
102
+ limits=TABULAR_LIMITS,
103
+ ),
104
+ )
105
+
106
+ KUMO_RELATIONAL = ModelSpec(
107
+ model_id=TFM_MODEL_KUMO_RELATIONAL,
108
+ profile=RELATIONAL,
109
+ runtime=RuntimeContract(
110
+ tasks=frozenset(
111
+ {
112
+ 'classification',
113
+ 'binary_classification',
114
+ 'multiclass_classification',
115
+ 'regression',
116
+ 'forecasting',
117
+ 'temporal_link_prediction',
118
+ }
119
+ ),
120
+ outputs=frozenset(
121
+ {
122
+ 'prediction',
123
+ 'probabilities',
124
+ 'scores',
125
+ 'rankings',
126
+ 'embeddings',
127
+ 'quantiles',
128
+ 'explanation',
129
+ }
130
+ ),
131
+ # Empty because nothing here takes a format argument: the contract's
132
+ # `OutputSpec` is only `{fields: [...]}`, and the endpoint names the
133
+ # shape it returns. Two names declared here appeared in no response.
134
+ explanation_formats=(),
135
+ limits=RELATIONAL_LIMITS,
136
+ unread_feature_stypes=frozenset({'text'}),
137
+ # Inference-config keys this client forwards, rather than keys the
138
+ # deployment is known to serve. The engine also sends `output_type`,
139
+ # whose choice travels as `output.prediction_statistic` instead, and
140
+ # `target_transforms`, which does nothing on this model: the endpoint
141
+ # accepts only `quantile`, reads it nowhere, and says the model emits
142
+ # its quantiles natively. A deployment may still refuse a value given
143
+ # for a key that is forwarded, and the shipping one refuses every
144
+ # shuffle and every `num_estimators` but its own, so this list is not
145
+ # a menu.
146
+ inference_options=frozenset(
147
+ {
148
+ 'kind',
149
+ 'num_estimators',
150
+ 'column_shuffle',
151
+ 'category_shuffle',
152
+ 'hop_shuffle',
153
+ 'class_shuffle',
154
+ }
155
+ ),
156
+ ),
157
+ )
158
+
159
+
160
+ class Model:
161
+ r"""Base for the model handles. Not useful on its own."""
162
+
163
+ _spec: ModelSpec
164
+
165
+ @property
166
+ def name(self) -> str:
167
+ r"""The model id sent on the wire."""
168
+ return self._spec.model_id
169
+
170
+ def __eq__(self, other: object) -> bool:
171
+ return isinstance(other, Model) and other._spec is self._spec
172
+
173
+ def __hash__(self) -> int:
174
+ return hash(self._spec.model_id)
175
+
176
+ def __repr__(self) -> str:
177
+ return f'{type(self).__name__}()'
178
+
179
+
180
+ class KumoTabular(Model):
181
+ r"""One table in, a prediction per row out.
182
+
183
+ Takes ``classification`` or ``regression``, and produces ``prediction``,
184
+ ``probabilities`` and ``quantiles``. It reads no relationships, so a
185
+ request carrying related tables is refused before it is sent.
186
+
187
+ Needs no extra: this model does no client-side work beyond shaping the
188
+ request, so it ships in the base install and runs anywhere.
189
+ """
190
+
191
+ _spec = KUMO_TABULAR
192
+
193
+
194
+ class KumoRelational(Model):
195
+ r"""Several related tables, joined as a graph.
196
+
197
+ Takes every task kind the contract carries except ``ranking``, and produces
198
+ all seven output fields including ``explanation``.
199
+
200
+ Needs ``pip install structured-data-models-client[relational]``: building the graph and
201
+ sampling it is heavy client-side work that lives in a native package.
202
+ """
203
+
204
+ _spec = KUMO_RELATIONAL
205
+
206
+
207
+ #: Every model this client can send to, by id.
208
+ MODELS = {
209
+ KUMO_TABULAR.model_id: KUMO_TABULAR,
210
+ KUMO_RELATIONAL.model_id: KUMO_RELATIONAL,
211
+ }
212
+
213
+
214
+ def spec_for(model: str | Model) -> ModelSpec:
215
+ r"""The specification behind a model handle or a model id.
216
+
217
+ Shared by the two places a model is named, gathering and sending a request,
218
+ so both accept the same arguments and refuse the same ones the same way.
219
+
220
+ A handle names one of the models below, not a new one. It is worth exactly
221
+ the spec it carries. That spec chooses the route a request is gathered for
222
+ and the limits it is checked against. The id
223
+ it names is a closed enum on the wire. One this module did not declare
224
+ is refused here rather than gathering work that cannot be sent. ``Model``
225
+ itself declares ``_spec`` without defining one, so a bare ``Model()`` gets
226
+ that same refusal rather than an ``AttributeError``.
227
+
228
+ Args:
229
+ model: A model handle such as ``KumoTabular()``, or a model id.
230
+
231
+ Raises:
232
+ UnknownModelError: If ``model`` is an id or handle the contract does
233
+ not carry.
234
+ StructuredDataError: If ``model`` is neither a handle nor an id.
235
+ """
236
+ if isinstance(model, Model):
237
+ spec = getattr(model, '_spec', None)
238
+ if isinstance(spec, ModelSpec) and MODELS.get(spec.model_id) is spec:
239
+ return spec
240
+ named = getattr(spec, 'model_id', None) or type(model).__name__
241
+ raise UnknownModelError(named, list(MODELS))
242
+ if isinstance(model, str):
243
+ spec = MODELS.get(model)
244
+ if spec is None:
245
+ raise UnknownModelError(model, list(MODELS))
246
+ return spec
247
+ raise StructuredDataError(
248
+ f'model must be a model handle or a model id, got '
249
+ f'{type(model).__name__}',
250
+ code='INVALID_REQUEST',
251
+ )
sdm_client/py.typed ADDED
File without changes