Vis

Decision models

Vis can answer typed choice, score and noul questions with the Laya ModernBERT decision model. Each answer also includes an action-versus-escalation score. The published baseline is a starting point, not a policy for taking actions on your behalf: collect representative labels for your use case, evaluate both heads and decide when a human should review the result before relying on it.

The assets-pack release keeps the existing voice assets and adds pinned FP32 inference bundles, complete checkpoints and offline training dependencies for Laya and both GLiNER2.5 decision models. Downloads are explicit; starting a gateway never downloads weights. Inside Vis Python, a small first-party client reads and infers without installing the full Python SDK or any third-party packages. Install the full vis-agent SDK separately for training, upload, aliases and remote clients.

Download the baseline

Install Vis, then download the pinned inference bundle on the machine running your gateway:

vis-agent decisions models status
vis-agent decisions models download --model laya-typed-decisions

The status command prints the model revision and whether the FP32 bundle is installed. The download verifies the archive and its file inventory before installation. To train on that machine, download the additional checkpoint and platform-specific CPython 3.12 wheelhouse:

vis-agent decisions models download --model laya-typed-decisions --training

The command prints the three installation directories and an offline install.sh command for a new Python environment. The wheelhouse supports macOS arm64 and Linux x86-64 with CPython 3.12. Obtain the matching vis-agent SDK wheel while online; the wheelhouse contains its training dependencies, not the SDK wheel. Run the printed installer, install that SDK wheel into the environment, then disconnect it from the network if needed. Allow several gigabytes of disk space for the checkpoint, FP32 bundle, training environment and exported versions. Review model licenses and provenance in THIRD_PARTY_MODELS.md.

For a client on another machine, use a secured gateway. Only the gateway needs the inference bundle. It will refuse a missing model instead of downloading one during a request.

Ask from Vis Python

Once the gateway machine has the inference bundle, you can ask typed questions in a python_execution block without installing a Python package into the sandbox:

import vis_decisions

print(vis_decisions.models())  # Known versions and their installed/resident states.
answer = vis_decisions.infer(
    model="laya-typed-decisions",
    state="A damaged item needs a refund",
    questions={
        "intent": {"type": "choice", "instructions": "Choose a request", "criteria": ["refund", "repair"]},
        "urgency": {"type": "score", "instructions": "Rate urgency", "criteria": ["low", "medium", "high"]},
        "refundable": {"type": "noul", "instructions": "Can the item be refunded?"},
    },
)
print(answer["answers"], answer["routing"]["model"])

vis_decisions.model("laya-typed-decisions") reads one model's status. These calls use Vis' existing gateway connection and authentication. They do not download a model or install a dependency. An uninstalled model raises vis_decisions.DecisionGatewayError with status == 409; download the model explicitly with the CLI above. The baseline act_probability in an answer is a diagnostic score, not authorization to act.

The separate py extension REPL uses your project's Python interpreter, not the python_execution sandbox. To call Decisions there or from an external Python app, install the full vis-agent SDK in that environment and use the Decisions and GatewayClient example below. Only install [decisions-training] when you need local training or export; neither installation downloads model weights automatically.

Train locally with the Python SDK

Install vis-agent[decisions-training] with a supported Python 3.12 environment (or use the verified offline wheelhouse and SDK wheel). TrainingBundle.open reads the complete checkpoint printed by the CLI; TrainingBundle.fetch(model_ref="laya-typed-decisions@<revision>", cache_dir="...") is a separate, explicit catalog-pinned download when you have network access. Neither open nor finetune fetches weights.

Make separate training and evaluation JSONL files. Every line needs state, one Laya question, an integer target option index and action (0 for act, 1 for escalate). For example, a training line can be:

{"state":"A damaged item needs a refund","question":{"type":"choice","instructions":"Choose a request","criteria":["refund","repair"]},"target":0,"action":1}

Use different labeled examples in eval.jsonl. You can also label score and noul questions. Put your training settings in config.json and minimum held-out accuracies for both heads in policy.json:

{"epochs":1,"learning_rate":0.00001,"train_encoder":false,"max_steps":100}
{"min_decision_accuracy":0.8,"min_action_accuracy":0.8}

Choose thresholds from your own evaluation, not the example values. Then train, export FP32 and reopen the graph for validation in one call:

from blockether.vis.decisions.training import ModernBertTrainer, TrainingBundle

base = TrainingBundle.open("/path/printed/by/download/training")
with ModernBertTrainer(base) as trainer:
    result = trainer.finetune(
        train_data="train.jsonl",
        eval_data="eval.jsonl",
        training_config="config.json",
        validation_policy="policy.json",
        output_dir="my-new-version",  # must not exist yet
        progress=lambda event: print(event["stage"]),
    )
print(result.checkpoint_dir, result.inference_bundle, result.validation_report)

result.checkpoint_dir is a complete resumable checkpoint; open it with TrainingBundle.open to train further. The separate inference_bundle contains ONNX FP32, tokenizer, configuration and provenance, not private training rows or checkpoint weights. To export an existing checkpoint without training, call trainer.prepare_fp32(eval_data=..., validation_policy=..., output_dir=...). An export or quality failure does not produce a deployable result, though a successfully saved checkpoint can remain for diagnosis. Training and export consume substantial local CPU, RAM and disk; the extra is not needed for inference-only clients.

Train either GLiNER2.5 decision model

You can choose the ordinary GLiNER2.5 base model or GLiNER2.5-Decide instead of Laya. Download the pinned FP32 bundle for the model you want to use on the gateway:

vis-agent decisions models download --model gliner2.5-base
# Or choose the other model:
vis-agent decisions models download --model gliner2.5-decide

For training, explicitly download the same model's complete checkpoint and the shared, platform-specific GLiNER training wheelhouse:

vis-agent decisions models download --model gliner2.5-base --training
# Or train the other model:
vis-agent decisions models download --model gliner2.5-decide --training

Each command prints the installed paths and an offline install.sh command for a new Python 3.12 environment. The GLiNER wheelhouse supports macOS 14+ arm64 or Linux x86-64 with glibc 2.28+ and CPU-only PyTorch. Allow several gigabytes for checkpoints, exported versions and the training environment. Keep GLiNER in a separate environment from Laya: install the matching vis-agent SDK wheel there before going offline (or install vis-agent[decisions-gliner-training] while online). Its Transformers 4 requirements conflict with Laya's Transformers 5. The wheelhouse contains pinned dependencies, not the SDK wheel; neither installation downloads weights.

Use GlinerTrainingBundle.open on the printed training directory. When you have network access, GlinerTrainingBundle.fetch can explicitly download a catalog-pinned model_ref (<model>@<revision>) into a cache_dir. Neither open nor the trainer fetches a model.

The JSONL rows and separate held-out data have the same state, question, target and action fields as the Laya example above. Keep training and held-out inputs distinct. GLiNER uses its own configuration; for example, {"epochs":1,"max_steps":100,"encoder_lr":0.00001,"task_lr":0.0005}. Set both minimum accuracies in policy.json from your use case, not from a baseline model. Then run:

from blockether.vis.decisions.gliner_training import GlinerTrainer, GlinerTrainingBundle

base = GlinerTrainingBundle.open("/path/to/gliner-training")
with GlinerTrainer(base) as trainer:
    result = trainer.finetune(
        train_data="train.jsonl",
        eval_data="eval.jsonl",
        training_config="gliner-config.json",
        validation_policy="policy.json",
        output_dir="my-gliner-version",  # must not exist yet
    )
print(result.checkpoint_dir, result.inference_bundle, result.validation_report)

You can reopen result.checkpoint_dir with GlinerTrainingBundle.open in a new process and train or export again without network access. To export without training, call trainer.prepare_fp32 on a complete checkpoint with eval_data, validation_policy and a new output_dir. Use Decisions.upload_model(result) and the same version, alias and inference calls below. The archive sent to the gateway contains only ONNX inference files; it does not include the checkpoint or labeled examples. GLiNER inference here covers decision classification and act/escalate, not entity or JSON extraction. Export and training require substantial CPU, RAM and disk. These weights are not approved for autonomous actions without representative, held-out validation.

Publish explicitly and select a version

Configure VIS_GATEWAY_URL and VIS_GATEWAY_TOKEN as described in the Python SDK gateway guide. Do not put the token in code or logs. The gateway verifies the streamed, inference-only bundle and runs both heads before registering an immutable sha256-... version. Upload never changes a running alias.

import os

from blockether.vis.decisions import Decisions
from blockether.vis.engine import GatewayClient

with GatewayClient(os.environ["VIS_GATEWAY_URL"], token=os.environ["VIS_GATEWAY_TOKEN"]) as gateway:
    decisions = Decisions(gateway)
    print(decisions.list_models())
    published = decisions.upload_model(result, progress=lambda sent, total: print(sent, total))
    ref = published["model_ref"]
    print(decisions.get_model(ref))
    decisions.activate_model("my-case", ref)  # creates a previously unused alias
    answer = decisions.infer(
        model="my-case",
        state="A damaged item needs a refund",
        questions={
            "intent": {"type": "choice", "instructions": "Choose a request", "criteria": ["refund", "repair"]},
            "urgency": {"type": "score", "instructions": "Rate urgency", "criteria": ["low", "medium", "high"]},
            "refundable": {"type": "noul", "instructions": "Can the item be refunded?"},
        },
    )
    print(answer["answers"], answer["routing"])

To switch an existing alias, read decisions.get_alias("my-case")["model_ref"] and pass it as expected_current=... to activate_model; a concurrent change returns a conflict instead of silently overwriting it. You can always infer with the immutable ref or the original laya-typed-decisions baseline independently. If an upload times out, get_model("sha256-" + archive_digest) lets you check whether the immutable version was registered before deciding to retry. Do not publish private training checkpoints or row files as model assets.

Train on the gateway instead

Set VIS_DECISION_TRAINING_PYTHON to a Python 3.12 executable with the SDK and vis-agent[decisions-training] for Laya. For GLiNER2.5 base or Decide, use a separate environment with vis-agent[decisions-gliner-training] and set VIS_DECISION_GLINER_TRAINING_PYTHON to its executable. The two training extras pin incompatible Transformers versions; the gateway never substitutes one interpreter or model family for the other. Set VIS_DECISION_TRAINING_DATA_ROOT to a directory of approved JSONL/JSON files on the gateway. Download the pinned checkpoint for the selected model explicitly with --training before starting.

The API accepts filenames in that directory, not laptop paths or raw uploads. Each dataset is limited to 16 MiB; configuration and policy files to 16 KiB. One training job runs at a time, for up to two hours by default. Other decision inferences are temporarily refused while the trainer owns the model budget; existing aliases and their selected versions remain unchanged.

job = decisions.start_training(
    train_data="train.jsonl", eval_data="eval.jsonl",
    training_config="config.json", validation_policy="policy.json",
)
print(decisions.get_training_job(job["job_id"]))
# Call get_training_job again to observe stages, steps, metrics and model_ref.
# decisions.cancel_training_job(job["job_id"]) cancels a running job;
# after completion, it deletes the private resumable checkpoint.

For GLiNER, pass model_id="gliner2.5-base" or "gliner2.5-decide" to decisions.start_training(...) with approved data and a GLiNER training config. The default without model_id stays Laya. Job status includes model_id, stage, progress, metrics and the eventual model_ref. The gateway stages bounded inputs, launches an isolated offline CPU worker, saves a private checkpoint and validates a new FP32 inference version. To continue from it, pass the completed or failed job_id as source_job_id with the same model_id; do not delete that job first. Cross-family resumes fail rather than falling back to another checkpoint.

Training does not activate an alias. Review the held-out metrics and use activate_model separately. A quality failure, interruption or cancellation leaves existing versions and aliases unchanged. Metrics on a small sample do not establish domain safety or authorize autonomous actions.

See also