Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ DATAROBOT_ENDPOINT=https://app.datarobot.com/api/v2
DATAROBOT_API_TOKEN=

# JointFM compatibility metadata for the selected deployment.
JOINTFM_SCHEMA_VERSION=v1
JOINTFM_SCHEMA_VERSION=v3
# Optional drift-detection pin; the SDK discovers the model version from /healthz when unset:
# JOINTFM_MODEL_VERSION=jointfm-inference:0.2.0+ckpt.fin-2026-05-22

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Agents must ground their thoughts in facts, not assumptions: before planning, claiming, or editing, read the relevant material — source, configs, data, docs, test output — and only act on beliefs backed by something just read or run. Agents must run ad-hoc Python and CLI commands through `task run -- <command>` (which wraps `uv run` with the project's canonical parameters), never bare `python` or hand-written `uv run` invocations — this keeps every invocation from accidentally re-resolving or mutating the `.venv` and `uv.lock`. Agents must explain what each command will do and why it is being run before running it. Before asking the user a question, agents must first explain the corresponding context and terminology — what the question concerns, why it arises, and what any project-specific terms mean — so the user can answer without digging through the code themselves. Agents must run `task pre-commit` and fix all reported issues before reporting success to the user. After `task pre-commit` succeeds, show the diff and explain why each change is necessary before reporting success to the user. Agents must never perform destructive or state-changing git operations unless the user explicitly instructs the agent to run that specific operation — no `git push --force`, no `git reset --hard`, no `git stash` (which hides uncommitted work), no branch/tag deletion, no history rewrites (`rebase`, `commit --amend` on published commits, `filter-branch`), no `git clean -fdx`, no `--no-verify` to bypass hooks, and no discarding of uncommitted work. Read-only inspection commands (`git status`, `git diff`, `git log`, `git show`) are always allowed. If a task seems to require a state-changing git operation and the user has not explicitly asked for it, stop and ask the user to run it. Agents must never mention temporary planning identifiers — phase names or numbers such as "Phase 3g", milestone, sprint, or ticket codes — in docstrings, comments, help or description strings, error messages, or test docstrings; those labels are deleted when the plan is retired and leave readers with a dangling reference nobody can decode, so describe the concept by its lasting behavior or stable configuration key instead (phase names belong only in roadmap and planning docs, and linking to such a doc by its actual filename is fine). Agents must not mention or recommend key rotation (rotating API keys, tokens, or other credentials) — the user takes care of key rotation themselves. Agents must keep comments in configuration files value-independent: a comment attached to a configuration parameter — in `config.yaml`/`config.sample.yaml`, a `Taskfile`, `pyproject.toml`, or any other settings file — may only state what remains true whatever the value is (what the parameter controls, what its options mean, which invariant or trade-off it participates in), and must never restate the current value, derive from it, or do arithmetic on it (`# 64 of 576 rows`, `# 2x the 288-step horizon`, `# keeps 90% of draws at full context`); configuration is retuned constantly, so such a comment turns false the moment somebody edits the value on the line below it and nothing catches the drift, so express a needed derivation as a rule over the parameter or record concrete numbers in a dated notes doc where they read as a historical observation. ripgrep (`rg`) is available (installed by `task setup` via `task install:ripgrep`); prefer it for fast code and text search.
Agents must ground their thoughts in facts, not assumptions: before planning, claiming, or editing, read the relevant material — source, configs, data, docs, test output — and only act on beliefs backed by something just read or run. Agents must run ad-hoc Python and CLI commands through `task run -- <command>` (which wraps `uv run` with the project's canonical parameters), never bare `python` or hand-written `uv run` invocations — this keeps every invocation from accidentally re-resolving or mutating the `.venv` and `uv.lock`. Agents must explain what each command will do and why it is being run before running it. Before asking the user a question, agents must first explain the corresponding context and terminology — what the question concerns, why it arises, and what any project-specific terms mean — so the user can answer without digging through the code themselves. Agents must run `task pre-commit` and fix all reported issues before reporting success to the user. After `task pre-commit` succeeds, show the diff and explain why each change is necessary before reporting success to the user. When what an agent reports is a set of items measured or classified along the same few dimensions — files and their status, config keys and their values, metrics before and after, options and their trade-offs — it must be presented as a Markdown table instead of prose or a bullet list, with short cells and only the columns that carry information; prose stays the right form for a single item, a narrative explanation, or an ordered procedure, because a one-row table, or one whose cells are sentences, is harder to read than the paragraph it replaced. Agents must never perform destructive or state-changing git operations unless the user explicitly instructs the agent to run that specific operation — no `git push --force`, no `git reset --hard`, no `git stash` (which hides uncommitted work), no branch/tag deletion, no history rewrites (`rebase`, `commit --amend` on published commits, `filter-branch`), no `git clean -fdx`, no `--no-verify` to bypass hooks, and no discarding of uncommitted work. Read-only inspection commands (`git status`, `git diff`, `git log`, `git show`) are always allowed. If a task seems to require a state-changing git operation and the user has not explicitly asked for it, stop and ask the user to run it. Agents must never mention temporary planning identifiers — phase names or numbers such as "Phase 3g", milestone, sprint, or ticket codes — in docstrings, comments, help or description strings, error messages, or test docstrings; those labels are deleted when the plan is retired and leave readers with a dangling reference nobody can decode, so describe the concept by its lasting behavior or stable configuration key instead (phase names belong only in roadmap and planning docs, and linking to such a doc by its actual filename is fine). Agents must not mention or recommend key rotation (rotating API keys, tokens, or other credentials) — the user takes care of key rotation themselves. Agents must keep comments in configuration files value-independent: a comment attached to a configuration parameter — in `config.yaml`/`config.sample.yaml`, a `Taskfile`, `pyproject.toml`, or any other settings file — may only state what remains true whatever the value is (what the parameter controls, what its options mean, which invariant or trade-off it participates in), and must never restate the current value, derive from it, or do arithmetic on it (`# 64 of 576 rows`, `# 2x the 288-step horizon`, `# keeps 90% of draws at full context`); configuration is retuned constantly, so such a comment turns false the moment somebody edits the value on the line below it and nothing catches the drift, so express a needed derivation as a rule over the parameter or record concrete numbers in a dated notes doc where they read as a historical observation. ripgrep (`rg`) is available (installed by `task setup` via `task install:ripgrep`); prefer it for fast code and text search.
60 changes: 48 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The SDK targets the DataRobot-hosted unstructured prediction route and the same
- Import namespace: `jointfm_client`
- Supported Python: `>=3.11`
- Current SDK package version: `0.7.0`
- Current JointFM service schema: `schema_version="v2"`
- Current JointFM service schema: `schema_version="v3"`

The public API shape is a synchronous low-level `JointFMClient` with `health()`, `health_instances()`, and `predict(payload)` methods plus high-level `forecast(...)`, `forecast_mean(...)`, `forecast_samples(...)`, and `forecast_quantiles(...)` helpers. The SDK is not a proxy service; callers use it as a local Python library that talks to the hosted or local JointFM endpoint.

Expand Down Expand Up @@ -50,7 +50,7 @@ Example deployment configuration:
deployment:
datarobot_endpoint: https://app.datarobot.com/api/v2
datarobot_api_token: <token>
schema_version: v2
schema_version: v3
deployment_id: <deployment-id>
# Optional model-version pin; the SDK discovers it from /healthz when unset:
# model_version: jointfm-inference:0.3.0+ckpt.fin-2026-05-22
Expand All @@ -68,7 +68,7 @@ Equivalent `.env` deployment configuration:
```dotenv
DATAROBOT_ENDPOINT=https://app.datarobot.com/api/v2
DATAROBOT_API_TOKEN=<token>
JOINTFM_SCHEMA_VERSION=v2
JOINTFM_SCHEMA_VERSION=v3
JOINTFM_DEPLOYMENT_ID=<deployment-id>
# Optional drift-detection pin; the SDK discovers the model version from /healthz when unset:
# JOINTFM_MODEL_VERSION=jointfm-inference:0.3.0+ckpt.fin-2026-05-22
Expand All @@ -78,7 +78,7 @@ Equivalent local REST configuration for a service started from the `joint` repos

```dotenv
JOINTFM_LOCAL_BASE_URL=http://127.0.0.1:8080
JOINTFM_SCHEMA_VERSION=v2
JOINTFM_SCHEMA_VERSION=v3
# Optional drift-detection pin; the SDK discovers the model version from /healthz when unset:
# JOINTFM_MODEL_VERSION=jointfm-inference:0.3.0+ckpt.fin_i504_o63_f0_t10_h16l16_mam7_af_t3r1_cnn_k3l4_hpst_h16l2_studentt_m4cr2df8skew
```
Expand Down Expand Up @@ -163,19 +163,55 @@ bootstrap_notebook(add_src_root=True)

Run `task setup` first so VS Code can select the registered `Python (joint-client-python)` notebook kernel backed by this repository's `.venv`.

The bootstrap helper resolves the nearest src-layout Python project root, switches the working directory there, and prepends that project's local `src` tree during development. The examples cover hosted health checks, low-level JSON prediction, mean forecasts, sample forecasts, quantile forecasts, pandas/NumPy result conversion, and CSV forecast workflows. They use `.env.sample` placeholders and checked-in fixture payloads; no real tokens or deployment IDs are stored in notebooks.
The bootstrap helper resolves the nearest src-layout Python project root, switches the working directory there, and prepends that project's local `src` tree during development. The examples cover hosted health checks, low-level JSON prediction, mean forecasts, sample forecasts, quantile forecasts, conditional forecasts, pandas/NumPy result conversion, and CSV forecast workflows. They use `.env.sample` placeholders and checked-in fixture payloads; no real tokens or deployment IDs are stored in notebooks.

The current forecast request contract is:

- `schema_version`: exactly `"v2"`, configured as `JOINTFM_SCHEMA_VERSION` for `from_env()` clients
- `schema_version`: exactly `"v3"`, configured as `JOINTFM_SCHEMA_VERSION` for `from_env()` clients
- `model_version`: exact model version advertised by `/healthz` or otherwise selected by the caller. Optional for `from_env()` clients: when `JOINTFM_MODEL_VERSION` is unset the SDK reads it from `/healthz` on first use; when set it acts as a drift-detection pin
- `query_mode`: `"forecast"`
- `query_mode`: `"forecast"` for the unconditional forecast, or `"condition"` for a conditional query at one future position; the high-level helpers set it from whether a `condition` block was passed
- `return_mode`: one of `"mean"`, `"samples"`, or `"quantiles"`
- `time_index_mode`: one of `"ordinal"`, `"continuous_float"`, or `"absolute_datetime"`
- `time_column`: required for `"absolute_datetime"`, and used for ordered ordinal or continuous histories when supplied
- `query_times`: non-empty future forecast times only
- `requested_columns`: optional column names or integer column indices, with duplicates rejected
- `n_samples`: positive sample count for sampled forecasts and quantile estimation. When `return_mode="samples"` exceeds the `max_sample_count` advertised by the deployment's health metadata, `forecast_samples(...)` splits the request into capped prediction batches up front and returns one merged `SampleForecastResult`.
- `condition`: required with `query_mode="condition"` and forbidden otherwise. A `ConditionBlock` naming one future position by its index into `query_times` and one condition per column, see below.

### Conditional Queries

The `condition` query mode asks for the model's joint distribution at one future position *given* something about some of its columns at that same position. Conditioning relates columns to each other within one position and never across horizons, so the block names the position once and the response describes that position alone: `outputs.query_times` carries exactly one entry however many `query_times` the request listed.

A column carries at most one condition, of either kind:

- `EqualityCondition(column, value)` pins the column to a finite value. The pinned column leaves the read-out set, so it must not appear in `requested_columns`.
- `IntervalCondition(column, lower=None, upper=None)` confines the column to a range; `None` leaves that side open, and at least one side must be bounded. The column stays readable, and what comes back is its distribution inside the range.

Every column without a condition is a read-out column, and at least one must remain. Pass the block to `forecast(...)`, `forecast_mean(...)`, `forecast_samples(...)`, or `forecast_quantiles(...)`:

```python
from jointfm_client import ConditionBlock, EqualityCondition, IntervalCondition

block = ConditionBlock(
query_time_index=0,
conditions=[
EqualityCondition(column="equity_index_level", value=4780.0),
IntervalCondition(column="treasury_10y_yield", lower=0.041, upper=0.045),
],
)
result = client.forecast_mean(
history,
query_times=query_times,
requested_columns=["portfolio_nav", "realized_volatility"],
columns=plan.columns,
condition=block,
)
print(result.plausibility)
```

Whether a deployment can condition depends on the mounted checkpoint's head. `/healthz` advertises `condition` in `supported_query_modes` and the kinds it answers in `supported_condition_kinds` (empty when the mode is absent). The client checks that advertisement before sending, so a deployment that cannot condition is refused with `UnsupportedServiceContractError` rather than after a paid round trip; `require_condition_support(metadata, block)` exposes the same check.

A condition response carries a `plausibility` block: `equality_log_density` is the log density the model assigns to the pinned values and `region_log_probability` the log probability it gives the interval region, each `None` when the request carried no condition of that kind. They separate a confident answer from one conditioned on something the model finds implausible; the service reports them and never refuses on them. `diagnostics.condition_draws` counts the draws behind sampled outputs, and `diagnostics.interval_estimator` reports the numerical accounting (`points`, `effective_sample_size`) when more than one column carries an interval and the region probability had to be estimated.

Column descriptors support the server fields `name`, `modality`, `role`, `nullable`, `vocabulary_size`, `level_count`, `mapping`, `lower_bound`, `upper_bound`, `time_value_kind`, `time_value_scale_seconds`, `time_value_use_local_normalized_time`, `time_value_calendar_id`, and `time_value_timezone`.

Expand All @@ -187,11 +223,11 @@ uv add "jointfm-client[notebooks]"

Use `build_forecast_payload_from_dataframe(...)` when history is already in a pandas DataFrame. It can accept explicit `ColumnSpec` objects or infer basic numeric, categorical, ordinal, count, binary, and time-valued columns from the DataFrame plus role, mapping, nullable, and bounds hints. The helper emits `history_rows` in the same order as the service frame builder: `time_column` first when present, followed by the ordered modeled columns. `build_forecast_payload_from_arrays(...)` provides the same request path for two-dimensional NumPy-like arrays when callers already have array values and column metadata. `build_datetime_query_times(...)`, `build_ordinal_query_times(...)`, `build_continuous_query_times(...)`, and `validate_forecast_horizon(...)` perform local future-horizon validation before the SDK sends the request.

Successful forecast responses preserve `schema_version`, `image_version`, `model_version`, `checkpoint_version`, `head`, `query_mode`, `return_mode`, `outputs`, and `diagnostics`. Structured service errors use this shape:
Successful forecast responses preserve `schema_version`, `image_version`, `model_version`, `checkpoint_version`, `head`, `query_mode`, `return_mode`, `outputs`, `plausibility`, and `diagnostics`. Structured service errors use this shape:

```json
{
"schema_version": "v2",
"schema_version": "v3",
"errors": [
{
"code": "VALIDATION_ERROR",
Expand All @@ -202,11 +238,11 @@ Successful forecast responses preserve `schema_version`, `image_version`, `model
}
```

Known error codes are `VALIDATION_ERROR`, `SCHEMA_VERSION_MISMATCH`, `MODEL_VERSION_MISMATCH`, `INPUT_SIZE_EXCEEDED`, and `INTERNAL_ERROR`.
Known error codes are `VALIDATION_ERROR`, `UNSUPPORTED_HEAD_QUERY_COMBINATION`, `UNSUPPORTED_RETURN_MODE`, `SCHEMA_VERSION_MISMATCH`, `MODEL_VERSION_MISMATCH`, `INPUT_SIZE_EXCEEDED`, and `INTERNAL_ERROR`.

## Compatibility Policy

The SDK supports only `schema_version="v2"`. `validate_service_metadata()` checks `/healthz` metadata and raises typed compatibility errors before prediction if the service advertises a different schema, an unexpected model version, mode capabilities outside the recorded service contract, or an unsupported `decoding_strategy`.
The SDK supports only `schema_version="v3"`. `validate_service_metadata()` checks `/healthz` metadata and raises typed compatibility errors before prediction if the service advertises a different schema, an unexpected model version, mode capabilities outside the recorded service contract, or an unsupported `decoding_strategy`. Return modes and time-index modes must match the SDK's lists exactly. Query modes and condition kinds are derived by the service from the mounted head, so a deployment may advertise fewer of them than the SDK knows; it must advertise at least one query mode and nothing the SDK does not know.

Callers should pass an expected `model_version` when they already know which deployment artifact they intend to use. A mismatch is treated as a hard compatibility error rather than silently downgrading, guessing, or retrying another model.

Expand Down Expand Up @@ -248,7 +284,7 @@ Create `.env` from `.env.sample` or set the same values in your shell. A hosted
```dotenv
DATAROBOT_ENDPOINT=https://app.datarobot.com/api/v2
DATAROBOT_API_TOKEN=<token>
JOINTFM_SCHEMA_VERSION=v2
JOINTFM_SCHEMA_VERSION=v3
JOINTFM_DEPLOYMENT_ID=<deployment-id>
# Or: JOINTFM_DEPLOYMENT_IDS=chevron-id,research-id
# Optional drift-detection pin; the SDK discovers the model version from /healthz when unset:
Expand Down
2 changes: 1 addition & 1 deletion config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ transport:
- X-DataRobot-Execution-ID
user_agent_header: User-Agent
forecast:
schema_version: v2
schema_version: v3
query_mode: forecast
return_mode: mean
time_index_mode: ordinal
Expand Down
Loading
Loading