agentleFS
NVIDIAKnown publisherNVIDIA/structured-data-models-client/skills/relational/SKILL.md

sdm-relational

Skill12 starsChanged 17 days agoApache-2.0View on GitHub

Ask kumo-relational a question about an entity and the records linked to it: write the question as a predictive query over a schema of related tables, covering classification, regression, forecasting and link prediction. Triggers: kumo-relational, KumoRelational, PREDICT, FOR EACH, predictive query, PQL, Schema.from_data, Schema.from_source, Relationship, ranking, forecast, link prediction, explanation, Snowflake, Databricks, warehouse, metric view, semantic view, SQL, catalog.

  • Installs packages

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Leave primary_keys or time_columns out and they are inferred, with a warning naming anything doubtful and the override that settles it.

    line 138
  2. Very specific:

    Every batch is built before the first is sent, so one materialize() gathers at most 100,000 entities, in at most 100 calls.

    line 176

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Asking kumo-relational

An entity, and the records linked to it. The question is written as a predictive query, the client gathers each entity's neighbourhood, and the model reads that neighbourhood in one pass.

Load ../tabular/SKILL.md instead when the question is about rows of one table with no links.

Writing the query itself is in pql.md.

What it can be asked

Two different questions, and they have two different answers:

from sdm_client import SDMClient, KumoRelational

with SDMClient(url='http://localhost:8000') as client:
    client.capabilities(KumoRelational())  # what this client will send
    client.models()                        # what the endpoint advertises

capabilities() is the client's own table, tagged source='sdk', and needs no connection. It is the wider of the two: a deployment may refuse something the client is willing to ask. What the client will send:

kumo-relational
Tasksbinary_classification, classification, multiclass_classification, regression, forecasting, temporal_link_prediction
Outputsprediction, probabilities, quantiles, rankings, scores, embeddings, explanation
Several tablesyes
Relationshipsyes
Sessionsyes

The outputs narrow again at the endpoint, by task. The shipping deployment serves prediction and quantiles on a regression, prediction and probabilities on either classification, and nothing else — including no explanation, on any task:

422 UNSUPPORTED_OUTPUT output(s) ['explanation'] not supported by model
'kumo-relational' for task 'regression'; supported: ['prediction', 'quantiles']

So ask for an explanation, catch the refusal, and fall back. Do not build a flow that assumes one arrives. Do not assume one never does either: another relational deployment serves explanation, and the same code gets a parsed one back there, so the refusal belongs to the deployment and not to the request. SDMClient.in_process() serves explanation too, with the same result.explanations as a NIM. EXPLAIN PREDICT … means the same as outputs=['explanation']; passing both is allowed and redundant.

The task is not an argument. It follows from the query: see pql.md.

The whole interface

KumoRelational needs the engine, which is an extra:

pip install 'structured-data-models-client[relational]'

Without it the first call raises MissingExtraError naming that command.

import pandas as pd
from sdm_client import (
    KumoRelational,
    PredictiveQuery,
    Relationship,
    SDMClient,
    Schema,
)

users = pd.DataFrame({'user_id': range(12), 'segment': ['a', 'b'] * 6})
orders = pd.DataFrame(
    {
        'order_id': range(120),
        'user_id': [i % 12 for i in range(120)],
        'item': [i % 4 for i in range(120)],
        'price': [10.0 + i for i in range(120)],
        'ordered_at': pd.date_range('2026-01-01', periods=120, freq='D'),
    }
)
items = pd.DataFrame({'item': range(4), 'kind': list('abcd')})

schema = Schema.from_data(
    {'users': users, 'orders': orders, 'items': items},
    primary_keys={'users': 'user_id', 'orders': 'order_id', 'items': 'item'},
    time_columns={'orders': 'ordered_at'},
    relationships=[
        Relationship('orders', 'user_id', 'users', 'user_id'),
        Relationship('orders', 'item', 'items', 'item'),
    ],
)

request = PredictiveQuery(
    'PREDICT COUNT(orders.*, 0, 30, days) FOR EACH users.user_id', schema
).materialize(indices=[1, 2, 3], model=KumoRelational())

with SDMClient(url='http://localhost:8000', timeout=600) as client:
    answer = client.predict(request, model=KumoRelational())
print(answer.to_pandas())

Verified against a live endpoint:

   row_index  id  prediction
0          0   1    2.173828
1          1   2    2.181641
2          2   3    2.212891

id is the entity, row_index its position in indices. Join on the key when it is there, otherwise on row_index, and never on row order.

Describing the data

A Schema says which tables exist, how they are keyed, which column carries each table's time, and how they link.

Schema.from_data(frames, primary_keys=, time_columns=, relationships=)

Relationship(from_table, from_column, to_table, to_column) points from the many side to the one side: an order names a user, so the relationship is Relationship('orders', 'user_id', 'users', 'user_id').

Leave primary_keys or time_columns out and they are inferred, with a warning naming anything doubtful and the override that settles it. The inference deliberately refuses some columns, among them a stamp recording when a row was last edited on a table other rows point at, since that moves after the fact and would leak the future into the past. Pass time_columns={'t': None} to say a table has no time column at all.

For tables that stay in a warehouse, Schema.from_source describes them in place and materialize() samples them with SQL, so what leaves is bounded rather than the table: up to a thousand rows of each for reading its columns -- all of one that small -- and after that only the rows a prediction needs. Snowflake, Databricks, Postgres, DuckDB and SQLite, metric views, and a notebook's own session are in references/warehouses.md.

Which rows get scored

materialize(indices=[...]) names the entities. It gathers each one's neighbourhood, checks every limit before sending, and refuses with the limit it would break rather than letting the endpoint return a 500.

LimitValue
context rows, by default1,000
context rows, with num_context_samples10,000
rows scored at once10,000
rows scored for a link prediction200
tables in the gathered graph15
hops gathered6
classes10

More rows than one call scores are not refused. They are gathered in batches of at most that many, and predict() sends one call per batch and returns one result, whose rows are numbered across the whole request, so to_pandas() gives the indices back in the order they were asked for. The 10,000 is the deployment's TFM_MAX_TEST_ROWS, not a property of the protocol; pass rows_per_call= to ask in smaller batches, which is what a graph too wide for a full one needs. It only goes down.

Every batch is built before the first is sent, so one materialize() gathers at most 100,000 entities, in at most 100 calls. Both are this client's own: the endpoint is never asked for more than one call holds. A question asked in parts is not batched at all: a ranking is already a call per candidate and a forecast one per step, and batching a part would send that many times as many calls.

The table limit is on the graph a request carries, not on the schema. A schema may describe far more than fifteen tables; what counts is how many the query actually reaches.

Asking for more than one answer each

A ranking is a call per candidate and a forecast a call per step; an explanation is an output, over every scored row. Each has limits and refusals of its own, and they are in references/beyond-one-answer.md.

An explanation is kept out of to_pandas(), on result.explanations.gradients, a mapping from each row's key to its explanation: the entity's id, or (id, forecast_step) on a forecast. One entry covers the entity's own row and every related row gathered for it, each under its table's name in related:

entry = result.explanations.gradients['user_5']  # the entity whose id is 'user_5'
entry['related']['users']              # the entity's own row, hop 0
orders = entry['related']['orders']    # one block per related table
entry['related']['users']['keys']      # present only when a relationship joins on the table's key column
orders['rows']                         # each row's values as sent
orders['gradients']                    # [output][row][column]
result.explanations.gradients.to_pandas()  # one line per (row, output, column), with its hop; the entity's users line is hop 0
result.explanations.columns.to_pandas()    # per-column cohorts of the labelled context

Asking the same context more than once

fit() uploads a context once and pins it in the endpoint's memory; every later call on the handle sends only the rows to score. Gathering still happens here, in materialize(), so what a session saves is sending the context again, which is worth doing when the same context answers question after question.

with SDMClient(url='http://localhost:8000', timeout=600) as client:
    fitted = client.fit(request, model=KumoRelational())
    print(fitted.predict(request).to_pandas())
    fitted.close()

Verified against a live endpoint:

   row_index  id  prediction
0          0   1    2.173828
1          1   2    2.181641
2          2   3    2.212891

The same answers the one-shot call gave: a session changes what it costs to ask again, not what comes back.

fit() takes a request of one call. A session pins one context, and a request gathered in several batches carries one per batch, so fitting one is refused. To score more rows than a call holds against a pinned context, fit a request of one call and pass the batched request to the handle's predict().

One context is one question. A later call on the handle has to ask the pinned query, task, target and entity table; anything else is refused and the refusal names which of them differs. Another question over the same tables is another fit().

A session lives in one process's memory on the endpoint, so a deployment behind several replicas needs the calls for one session to reach the same one.

The context is chosen once, when it is pinned, so a row scored later is sitting in it with its answer already shown. fit(request, model=..., holdout=ids) names every row the session will go on to score and holds all of them back while there is still a context to hold them out of. There is no remedy afterwards: a context cannot be changed once pinned. A ranking and a forecast cannot be fitted at all; see references/beyond-one-answer.md.

Reading the answer

predict() gives a PredictionResult. Its to_pandas() gives one row per entity, except a forecast, which gives one per entity per step, and a ranking, which gives a list in one row.

A regression's prediction is the median of the predicted distribution, not the mean. Ask for the mean with materialize(..., prediction_statistic='mean') when predictions will be summed across entities, since medians do not add up.

When it warns, and when it refuses

A warning is this client answering, but not as asked: a context row that carried the answer, an entity filter that does not narrow what is scored, a candidate it could not score. They are all SDMWarning, so they can be caught or raised:

import warnings
from sdm_client import SDMWarning

warnings.simplefilter('error', SDMWarning)

A refusal is a StructuredDataError or a subclass, and each names what to change. The ones worth knowing:

RefusalCause
every candidate's context holds one classeach gathered entity reached it, or none did, so there is nothing to tell apart and nothing is sent; widen with num_context_samples or ask over a table of pairs. One such candidate among several is dropped with a warning instead, and named on result.unscored_candidates
no entity existed at the anchorthe entity table's time column starts after the anchor, so nothing existed to ask about
more than 100,000 entities at onceevery batch is built before any is sent; ask about fewer at a time
more than 100 calls at oncea context is gathered per call; ask about fewer entities, or raise rows_per_call if you lowered it
more entities than one call scores, on a ranking or a forecasteach candidate or step is already a call, and batching one would send that many times as many; ask about fewer at a time
an output this question cannot giverankings on a regression, explanation on a ranking
422 UNSUPPORTED_OUTPUTthe client sent an output the deployment does not serve, such as explanation
an aggregation the model does not answerCOUNT_DISTINCT, FIRST, LAST, or any aggregation of a category

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.