SystemOneDotNet 1.0.1
dotnet add package SystemOneDotNet --version 1.0.1
NuGet\Install-Package SystemOneDotNet -Version 1.0.1
<PackageReference Include="SystemOneDotNet" Version="1.0.1" />
<PackageVersion Include="SystemOneDotNet" Version="1.0.1" />
<PackageReference Include="SystemOneDotNet" />
paket add SystemOneDotNet --version 1.0.1
#r "nuget: SystemOneDotNet, 1.0.1"
#:package SystemOneDotNet@1.0.1
#addin nuget:?package=SystemOneDotNet&version=1.0.1
#tool nuget:?package=SystemOneDotNet&version=1.0.1
SystemOneDotNet
Structured AI decisions for .NET — no prompt engineering, no output parsing.
SystemOneDotNet is the .NET client for TypeSafe System One, the class of models that Jev belongs to. System One models do not write prose. They evaluate a state against typed questions and return decisions, probabilities, and calibrated confidence that your code can branch on directly:
| Ask the model | Get back | C# answer type |
|---|---|---|
| Choice — pick one option | the chosen option, every option's probability, overall confidence | ChoiceAnswer<T> |
| Score — rate the state on an ordered scale | a probability-weighted score, the legend, probabilities, confidence | ScoreAnswer |
| Noul — is the statement true? | the probability that the answer is yes, from 0 to 1 |
NoulAnswer |
You declare the answer space in C# and get C# values back. One request can carry several questions, and every question is evaluated in parallel and in isolation against the same state — adding questions barely changes the response time. System One is a natural fit for triage, routing, moderation, lead scoring, and any decision you would otherwise hand-roll with regex or a generic LLM prompt. The default model alias is jev-latest.
Install
dotnet add package SystemOneDotNet
That is the whole setup. The package targets .NET Standard 2.0, so it runs on .NET Framework 4.6.2+, .NET Core 2.0+, and every modern .NET release. Its only dependency is System.Text.Json.
Grab an API key from the TypeSafe console. If you want to see the model answer before writing any code, try the Playground first. Then make your first call:
using SystemOneDotNet;
using SystemOneDotNet.Answers;
string apiKey = Environment.GetEnvironmentVariable("SYSTEMONE_API_KEY")
?? throw new InvalidOperationException("Set SYSTEMONE_API_KEY.");
using ISystemOneClient systemOne = SystemOneClient.Create(apiKey);
string ticket =
"Hi, I've been trying to connect my Stripe account for 3 days and it keeps failing. " +
"I'm losing sales. Please help ASAP.";
// Choose one option.
ChoiceAnswer<string> department = await systemOne.ChoiceAsync(
ticket,
"Which team should handle this?",
new[] { "billing", "technical", "sales" });
// Rate the state along an ordered scale.
ScoreAnswer frustration = await systemOne.ScoreAsync(
ticket,
"How frustrated is the customer?",
new[] { "Calm", "Frustrated", "Very angry" });
// Ask a yes/no question. The answer is a probability, never a bool.
NoulAnswer urgent = await systemOne.NoulAsync(
ticket,
"Does this message convey urgency?");
Console.WriteLine(department.Choice); // e.g. "technical"
Console.WriteLine(department.Confidence); // e.g. 0.78
Console.WriteLine(frustration.Score); // e.g. 1.0
Console.WriteLine(urgent.Noul); // e.g. 0.98
Every method accepts an optional CancellationToken:
var answer = await systemOne.ChoiceAsync(ticket, "Which team?", teams, cancellationToken: ct);
One request, many answers
Questions are created with the Question factory. They are immutable, validated once, carry no response
state, and can be reused across any number of requests. Send as many as you like in a single request — they
are evaluated in parallel against the same state, so adding questions barely changes the response time:
using SystemOneDotNet.Questions;
IChoiceQuestion<Team> department = Question.Choice(
"department",
"Which team should handle this?",
teams);
INoulQuestion urgent = Question.Noul(
"is_urgent",
"Does this message convey urgency?",
trueDescription: "The customer is blocked or losing money.",
falseDescription: "The request can wait until tomorrow.");
IScoreQuestion frustration = Question.Score(
"frustration",
"How frustrated is the customer?",
new[] { "Calm", "Frustrated", "Very angry" });
ISystemOneResult result = await systemOne.Query(ticket)
.Question(department)
.Question(urgent)
.Score(frustration)
.SendAsync(ct);
// Read the typed answers back with the same question handles.
ChoiceAnswer<Team> routing = result.Get(department);
double routed = routing.Confidence;
double score = result.Get(frustration).Score;
double probability = result.Get(urgent).Noul;
// Every batch reports the model and the token usage.
Console.WriteLine(result.Model); // "jev-1.x.y"
Console.WriteLine(result.Usage.InputTokens);
Console.WriteLine(result.Usage.OutputTokens);
Questions can be defined once next to your domain types and shared by every request. AskAsync is the
single-question shortcut for Query(...).Question(...).SendAsync():
ChoiceAnswer<Team> answer = await systemOne.AskAsync(ticket, department, ct);
Use your own types as options
Choice options do not have to be strings. Pass any values — POCOs, enums, or records — and each option is sent to the model as its JSON description, so the model reasons about the fields instead of opaque labels:
using System.Text.Json.Serialization;
public sealed record Team
{
public string Name { get; init; }
public string Email { get; init; }
[JsonIgnore]
public string SlackChannel { get; init; } // never sent to the model
}
var teams = new[]
{
new Team { Name = "Billing", Email = "billing@example.com", SlackChannel = "#billing" },
new Team { Name = "Technical", Email = "tech@example.com", SlackChannel = "#technical" },
new Team { Name = "Sales", Email = "sales@example.com", SlackChannel = "#sales" },
};
ChoiceAnswer<Team> routing = await systemOne.ChoiceAsync(
ticket,
"Which team should handle this?",
teams);
Console.WriteLine(routing.Choice.Name); // "Technical"
Console.WriteLine(routing.Choice.SlackChannel); // "#technical" — your original instance
The API receives the options as a criteria map with deterministic ids:
{
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {
"0": { "Name": "Billing", "Email": "billing@example.com" },
"1": { "Name": "Technical", "Email": "tech@example.com" },
"2": { "Name": "Sales", "Email": "sales@example.com" }
}
}
Answers map back to the original values, so routing.Choice is the exact Team instance that was
supplied — including members that were never sent to the model. No equality overrides or
dictionary-compatible keys required. Strings stay strings, and enums are serialized by name.
Named options
The named criteria dictionary from the TypeSafe quickstart is supported too, with optional descriptions:
INamedChoiceQuestion department = Question.NamedChoice(
"department",
"Which team should handle this?",
new Dictionary<string, string?>
{
["billing"] = "Payment or subscription issues",
["technical"] = "Bugs or integration problems",
["sales"] = null,
});
var result = await systemOne.Query(ticket).Question(department).SendAsync();
string selected = result.Get(department).Choice;
Answers you can act on
Every answer carries more than the decision itself, so your code can decide whether to act:
ChoiceAnswer<T>.Choiceis the original selected value;.Optionsis an ordered list ofChoiceProbability<T>pairs with the original values and their probabilities;.Confidenceis the model's confidence.ScoreAnswer.Scoreis the probability-weighted value,.Legendis the ordered level list,.Probabilitiesis populated when the API supplies the distribution, and.Confidenceis the model's confidence.NoulAnswer.Noulis the probability that the answer is yes, from 0 to 1. There is no automatic boolean conversion, so you choose the threshold.
Confidence turns a decision into a policy. Act automatically when the model is sure, and escalate when it is not:
ChoiceAnswer<Team> routing = await systemOne.ChoiceAsync(ticket, "Which team?", teams);
if (routing.Confidence < 0.9)
{
return EscalateToHuman(ticket); // not confident enough to route blindly
}
Notify(routing.Choice.SlackChannel);
The full distributions are there for ranking and analytics:
foreach (var option in routing.Options)
{
Console.WriteLine($"{option.Probability:P1} {option.Option.Name}");
}
if (frustration.Probabilities is not null)
{
foreach (var level in frustration.Probabilities)
{
Console.WriteLine($"{level.Probability:P1} {level.Level}");
}
}
Answers live in SystemOneDotNet.Answers and are plain records with public constructors, so a test double
for ISystemOneClient can return new NoulAnswer(0.9) directly.
Options
var options = new SystemOneOptions
{
Endpoint = "https://api.typesafe.ai/v1/systemone", // default
Model = "jev-latest", // default
MaxChoiceProperties = 20, // default
};
using ISystemOneClient systemOne = SystemOneClient.Create(apiKey, options, httpClient);
EndpointandModeloverride the API target.MaxChoicePropertieslimits how many serialized properties a single choice option may contain.SystemOneOptionsis immutable: the values are validated once when the client is constructed. Usewithto derive a modified copy, for exampleoptions with { MaxChoiceProperties = 10 }.- A supplied
HttpClientis reused and never disposed by the client; when none is supplied, the client owns and disposes its internalHttpClient. Use this to configure timeouts or proxies.
Choice limits
MaxChoiceProperties counts the properties in each option's serialized object tree, including nested
properties, dictionary entries, and properties inside array elements. JSON object/array values
(JsonNode, JsonElement) are counted exactly like their serialized form. Members marked with
[JsonIgnore] are not counted.
The limit defaults to 20 and is validated before any HTTP request is sent; options are never truncated. An oversized option produces an actionable error:
Choice option 2 contains 54 serialized properties; the limit is 20.
Use a dedicated smaller POCO or increase SystemOneOptions.MaxChoiceProperties
when creating the client.
Error handling
Failures surface as exceptions in SystemOneDotNet.Exceptions, all deriving from SystemOneException (cancellation surfaces as OperationCanceledException):
try
{
var answer = await systemOne.ChoiceAsync(ticket, "Which team?", teams);
}
catch (SystemOneValidationException ex) { /* local validation failed; no request was sent */ }
catch (SystemOneApiException ex) { /* non-success status; ex.StatusCode, ex.ResponseBody */ }
catch (SystemOneProtocolException ex) { /* malformed or mismatched answer */ }
SystemOneValidationException— local validation failed and no request was sent: missing or duplicate question ids, empty batches, fewer than two score levels, more than 255 choice options, null or cyclic options, an exceeded property limit, or a question that was not created by theQuestionfactory.SystemOneApiException— the API returned an unsuccessful status code.StatusCodeandResponseBodyare exposed.SystemOneProtocolException— the API returned a successful response that is malformed, missing an answer, has a mismatched answer type, or contains an unknown option id.OperationCanceledException— cancellation was requested. The originalCancellationTokenis propagated unchanged.
The client performs no automatic retries, caching, or blocking calls. A state that cannot be serialized
to JSON (for example a cyclic object graph) is reported locally as SystemOneValidationException before
any request is sent. To control timeouts, inject an HttpClient configured with your own Timeout.
Runnable sample
samples/SystemOneDotNet.Sample is a console app that walks
through the library against the live API: direct calls, structured options, batches, named options,
validation, and cancellation. Clone the repository, set an API key, and run it:
export SYSTEMONE_API_KEY="your-key"
dotnet run --project samples/SystemOneDotNet.Sample # every scenario
dotnet run --project samples/SystemOneDotNet.Sample -- batch # one scenario
Set SYSTEMONE_MODEL to override the default model, or pass --help to list the scenarios.
Design notes
SystemOneDotNet is a standalone client library, not a hosted service, so a few deliberate choices differ
from service-oriented .NET conventions:
- It targets
netstandard2.0so older runtimes can consume it.src/SystemOneDotNet/Internal/IsExternalInit.cssupplies the marker type that C# records andinitaccessors require on that target. - The public surface is interfaces and records only.
ISystemOneClient,ISystemOneQuery,ISystemOneResult, and theIQuestionfamily are interfaces with internal implementations; answers, token usage, andSystemOneOptionsare immutable records. The two static factories,SystemOneClient.CreateandQuestion, are the only entry points into the internals, and the exception hierarchy is the only other set of public classes. - Namespaces group the surface by role:
SystemOneDotNetholds the client, batch, result, and options;SystemOneDotNet.Questionsthe question interfaces and factory;SystemOneDotNet.Answersthe answer records;SystemOneDotNet.Exceptionsthe exception hierarchy. Everything underSystemOneDotNet.Internalis an implementation detail. - Callers depend on
ISystemOneClientand can substitute it in tests. There is no DI container, hosted service, orILoggerdependency.HttpClientis supplied throughSystemOneClient.Createinstead ofIHttpClientFactory, and the client disposes only the instance it created. Failures surface as theSystemOneExceptionhierarchy and callers decide how to log them. - Tests use xUnit, AwesomeAssertions, and NSubstitute; coverage is collected with coverlet.
Building and verifying
dotnet build -c Release
dotnet test
dotnet run --project tests/SystemOneDotNet.Verification
dotnet pack src/SystemOneDotNet -c Release
Collect coverage locally and enforce the same thresholds the CI pipeline uses:
dotnet test tests/SystemOneDotNet.Tests -c Release \
-p:CollectCoverage=true \
-p:CoverletOutputFormat=cobertura \
-p:CoverletOutput=./artifacts/coverage/ \
-p:Threshold=100%2C99 \
-p:ThresholdType=line%2Cbranch \
-p:ThresholdStat=total
%2C escapes the comma inside the MSBuild property values; the threshold order matches ThresholdType
(line, then branch). The report is written to artifacts/coverage/coverage.cobertura.xml. The unit tests
currently reach 100% line and 100% branch coverage, and CI fails when line coverage drops below 100% or
branch coverage drops below 99%.
tests/SystemOneDotNet.Testscontains the unit tests, written with xUnit, AwesomeAssertions, and NSubstitute.tests/SystemOneDotNet.Verificationis a small runnable verification program that exercises the library through a fakeHttpMessageHandler, without a test-framework dependency..github/workflows/ci.ymlbuilds the solution, runs the unit tests with the coverage thresholds, runs the verification runner, and validates that the library packs on every push tomainand every pull request. The coverage report is uploaded as a workflow artifact and summarized in the run page.
License
MIT. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Text.Json (>= 8.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.