How-to guide

Answer decisions with Nimble

Source Trax.Effect.Decisions.SystemOne/

This page assumes a train that already asks decisions. What the adapter sends and refuses is on Decision records.

1. Run a Nimble server

Start a server from Nimble's own serving code (nimble/serving/server.py in bespokelabsai/nimble). It answers POST /v1/systemone. It runs SGLang and needs an NVIDIA GPU; on a Mac with Apple Silicon, serve the model through Nimble's MLX scorer instead, as the Recovery sample does (Nimble on a Mac).

Pin the model revision where you deploy the server. The request cannot pin one, so the server answers with whatever it was deployed with.

2. Register the decider

using Trax.Effect.Data.Extensions;
using Trax.Effect.Data.Postgres.Extensions;
using Trax.Effect.Decisions.SystemOne.Extensions;
using Trax.Effect.Extensions;
 
services.AddTrax(trax => trax.AddEffects(effects => effects
    .UsePostgres(connectionString)
    .AddDecisionRecording()
    .AddNimbleDecider(o => o.Endpoint = new Uri("http://localhost:8000/v1/systemone"))));

Endpoint is required: there is no default, and the host does not start without one. A server on another machine must be HTTPS. When the server is started with OPENJEV_API_KEY, set ApiKey:

effects.AddNimbleDecider(o =>
{
    o.Endpoint = new Uri("https://nimble.internal.example/v1/systemone");
    o.ApiKey = configuration["Nimble:ApiKey"];
});

AddDecisionRecording is optional for answering, and required for step 4 and for replaying a run's answers. The other settings (MaxConcurrentRequests, MaxOptions, timeouts and retries) are on AddNimbleDecider.

Send a state that holds only what the questions need. These models answer less accurately with detail that does not bear on the question.

3. Put Nimble in front of a larger model (optional)

Register each model by name, then compose them into the IDecider every train asks. A question Nimble is unsure of goes to the second model:

using Trax.Core.Decisions;
using Trax.Effect.Decisions.SystemOne;
 
services.AddTrax(trax => trax.AddEffects(effects => effects
    .AddNimbleDecider("nimble", o => o.Endpoint = new Uri("http://localhost:8000/v1/systemone"))
    .AddSystemOneDecider("jev", o =>
    {
        o.Endpoint = new Uri("https://api.typesafe.ai/v1/systemone");
        o.Model = "jev-1.13.0";
        o.ApiKey = configuration["Jev:ApiKey"];
    })));
 
services.AddSingleton<IDecider>(sp => new CascadingDecider(
    sp.GetRequiredKeyedService<SystemOneDecider>("nimble"),
    sp.GetRequiredKeyedService<SystemOneDecider>("jev"),
    escalateBelow: 0.8));

Which answers escalate is on Escalating what the fast decider is unsure of. To use another System One model on its own, call AddSystemOneDecider without a name in place of AddNimbleDecider: moving between them is a change of endpoint and model name.

4. Tune the confidence bars

Check each switch's confidence bars against your own labelled cases, not a published benchmark. The recorded decisions are the data: this query counts how often each track was taken this week, and how often the decision was overruled.

-- question_key is the type's name without its namespace, unless [Asks] sets a Key.
SELECT r ->> 'track' AS track, count(*) AS routings, count(r ->> 'fallback_reason') AS overruled
FROM trax.decision d
JOIN trax.metadata m ON m.id = d.metadata_id
CROSS JOIN LATERAL jsonb_array_elements(d.routes) AS r
WHERE d.question_key = 'TicketTrack' AND m.start_time > now() - interval '7 days'
GROUP BY 1;

Label a few hundred recorded decisions with what should have happened, and pick the bar where the cost of a wrong track meets the cost of sending work to the fallback. Re-check the bars whenever the server's model revision changes.

SDK Reference

AddNimbleDecider | AddSystemOneDecider | AddDecisionRecording