Using Jev and TypeSafe System One with WebService::TypeSafe

This guide assumes basic Perl syntax but no prior experience with AI APIs.

WebService::TypeSafe is provided by Data Sculpting Inc. (info@datasculpting.com, https://datasculpting.com/).

1. How the API works

A System One request contains state (the data to evaluate), questions (named decisions Jev should make), and a model (normally jev-latest). Each answer returns under the same name as its question. Asking several questions in one request is the normal pattern.

2. Installation

The distribution requires Perl 5.20 or newer.

unzip WebService-TypeSafe-0.01.zip
cd WebService-TypeSafe-0.01
perl Makefile.PL
make
make test
make install

For a user-local installation:

perl Makefile.PL INSTALL_BASE="$HOME/perl5"
make
make test
make install
export PERL5LIB="$HOME/perl5/lib/perl5${PERL5LIB:+:$PERL5LIB}"

Run an example without installing with perl -Ilib examples/basic.pl.

3. Authentication

The primary interface is the api_key constructor option. Obtain the key from your application's configuration or secret manager, then pass it explicitly:

my $api_key = get_typesafe_api_key_from_your_config();

my $client = WebService::TypeSafe->new(
    api_key => $api_key,
);

This makes the client's dependency explicit, supports multiple clients with different credentials, and simplifies tests. Do not hard-code or commit the actual key.

Optional environment fallback

If api_key is omitted, the constructor falls back to TYPESAFE_API_KEY. This is convenient for command-line programs, containers, and CI environments.

Linux or macOS:

export TYPESAFE_API_KEY='your-api-key'

In Windows PowerShell:

$env:TYPESAFE_API_KEY = 'your-api-key'

With the fallback configured, this is equivalent:

my $client = WebService::TypeSafe->new;

An explicitly passed api_key always takes precedence over TYPESAFE_API_KEY.

4. Your first request

use strict;
use warnings;
use feature 'say';
use WebService::TypeSafe qw(noul);

my $api_key = get_typesafe_api_key_from_your_config();
my $client = WebService::TypeSafe->new(api_key => $api_key);
my $result = $client->system_one(
    state => 'Help! My payouts have failed for three days.',
    questions => {
        urgent => noul(
            instructions => 'Does this message convey urgency?',
        ),
    },
);

my $probability = $result->answers->{urgent}->noul;
say "Probability of yes: $probability";
say 'Route to urgent queue' if $probability >= 0.8;

5. Choosing a question type

Noul: yes or no

Use Noul when the probability of a true condition is useful.

my $question = noul(
    instructions => 'Does the customer explicitly request a refund?',
    criteria => {
        true  => 'The customer asks for money to be returned',
        false => 'No refund is requested',
    },
);

my $p_yes = $result->nouls->{refund_requested}->noul;

criteria is optional. Noul has no separate confidence: near 1 favors yes, near 0 favors no, and near 0.5 is uncertain.

Choice: one named alternative

Use Choice for unordered categories.

my $question = choice(
    instructions => 'Which team should handle this ticket?',
    criteria => {
        billing   => 'Invoices, charges, refunds, or payouts',
        technical => 'Bugs, outages, or integrations',
        sales     => 'Pricing, trials, or upgrades',
        other     => undef,
    },
);

my $answer = $result->choices->{department};
say $answer->choice;
say $answer->confidence;
say $answer->probabilities->{billing};

Include other if the list may not cover every input.

Score: an ordered scale

Use Score for a spectrum with 2–10 defined levels.

my $question = score(
    instructions => 'How frustrated is the customer?',
    criteria => [
        'Calm and neutral',
        'Concerned but civil',
        'Angry or using strong language',
    ],
);

my $answer = $result->scores->{frustration};
say $answer->score;              # May fall between levels
say $answer->confidence;
say $answer->legend->{0};
say $answer->probabilities->{2};

6. Reading responses

Every answer is in $result->answers->{question_name} and is also grouped by type in $result->nouls, $result->choices, or $result->scores.

say $result->model;
say $result->usage->input_tokens;
say $result->usage->output_tokens;

my $answer_hash = $result->answers->{urgent}->raw;

7. Structured state and instructions

State may be a string, hash reference, or array reference containing JSON-compatible values.

my $state = {
    ticket => {
        subject => 'Duplicate charge',
        messages => [
            { from => 'customer', text => 'I was charged twice. Refund it.' },
            { from => 'support',  text => 'We are checking.' },
        ],
    },
    order => {
        id => 'A-104',
        charges => [
            { amount_usd => 49, status => 'captured' },
            { amount_usd => 49, status => 'captured' },
        ],
    },
};

my $result = $client->system_one(
    state => $state,
    questions => {
        refund_requested => noul(
            instructions => 'Does `ticket.messages[0].text` request a refund?',
        ),
        duplicate_charge => noul(
            instructions => 'Do `order.charges` show a duplicate charge?',
        ),
    },
);

Instructions and criteria may themselves be strings, hashes, or arrays.

8. Confidence-based routing

Choice and Score answers include confidence. Your code decides what is safe enough for automatic action.

my $routing = $result->choices->{department};

if ($routing->confidence >= 0.85) {
    route_ticket($routing->choice);
}
else {
    send_to_human_review();
}

For Noul, use a review band:

my $p = $result->nouls->{safe_to_publish}->noul;
if    ($p >= 0.9) { publish() }
elsif ($p <= 0.1) { reject() }
else              { review() }

Tune thresholds using examples from your own application.

9. Client configuration

use WebService::TypeSafe qw(retry_policy);

my $client = WebService::TypeSafe->new(
    api_key  => $api_key,
    model    => 'jev-latest',
    base_url => 'https://api.typesafe.ai',
    timeout  => 30,
    retry    => retry_policy(max_retries => 3),
    headers  => { 'x-application-name' => 'support-router' },
);

The normal credential option is api_key. Its optional environment fallback is TYPESAFE_API_KEY. The other environment defaults are TYPESAFE_DEFAULT_MODEL and TYPESAFE_BASE_URL. Explicit constructor options take precedence over all environment values. A request may override model, timeout, retry, extra_headers, or extra_body.

my $result = $client->system_one(
    state => $state,
    questions => \%questions,
    timeout => 15,
    extra_headers => { 'x-trace-id' => $trace_id },
);

10. Retries and timeouts

By default, retryable failures receive two additional attempts with exponential backoff. Numeric Retry-After and retry-after-ms headers are honored.

my $policy = retry_policy(
    max_retries     => 4,
    backoff_initial => 0.5,
    backoff_max     => 8,
    backoff_jitter  => 0.25,
    timeout         => 30,  # Total retry budget
);

Disable retries with retry_policy(max_retries => 0). The client timeout is the HTTP operation timeout; the policy timeout is the overall retry budget.

11. Error handling

Exceptions are objects, but unrelated Perl code can still throw strings, so check ref($@) before calling methods.

my $result = eval {
    $client->system_one(state => $state, questions => \%questions);
};

if (my $error = $@) {
    if (ref($error) && $error->isa('WebService::TypeSafe::RateLimitError')) {
        warn 'Rate limited; request ID: ', ($error->request_id // 'unknown');
    }
    elsif (ref($error) && $error->isa('WebService::TypeSafe::AuthenticationError')) {
        die 'Check api_key or its TYPESAFE_API_KEY fallback';
    }
    elsif (ref($error) && $error->isa('WebService::TypeSafe::TimeoutError')) {
        warn 'Timed out after ', $error->timeout, ' seconds';
    }
    elsif (ref($error) && $error->isa('WebService::TypeSafe::APIError')) {
        warn 'HTTP status: ', $error->status;
        die $error;
    }
    else {
        die $error;
    }
}

Status-specific classes include BadRequestError (400), AuthenticationError (401), PermissionDeniedError (403), NotFoundError (404), UnprocessableEntityError (422), RateLimitError (429), and InternalServerError (5xx), all under WebService::TypeSafe::. Connection, timeout, and response-validation classes are also provided. API errors expose status, body, headers, endpoint, and request_id.

12. Listing models

my $response = $client->models->list;
for my $model (@{ $response->models }) {
    say $model->name;
    say '  ', $model->description if defined $model->description;
}

Use jev-latest unless you intentionally need a pinned model version.

13. Testing without calling the API

Inject an HTTP coderef to capture requests and return fixtures without network access or API charges:

use JSON::PP qw(encode_json);

my $client = WebService::TypeSafe->new(
    api_key => 'test-key',
    http => sub {
        my ($method, $url, $request) = @_;
        return {
            success => 1,
            status  => 200,
            headers => {},
            content => encode_json({
                model => 'jev-test',
                answers => {
                    urgent => { type => 'noul', noul => 0.95 },
                },
                usage => { input_tokens => 10, output_tokens => 2 },
            }),
        };
    },
);

See t/02-client.t and t/03-errors-retries.t for complete examples.

14. Common recipes

Route a support ticket

my $result = $client->system_one(
    state => { message => $ticket_text },
    questions => {
        department => choice(
            instructions => 'Which department should handle `message`?',
            criteria => {
                billing => 'Invoices, charges, or refunds',
                technical => 'Bugs, outages, or integrations',
                sales => 'Pricing or upgrades',
                other => undef,
            },
        ),
        urgent => noul(
            instructions => 'Does `message` need immediate attention?',
        ),
    },
);

my $queue = $result->choices->{department}->choice;
my $priority = $result->nouls->{urgent}->noul >= 0.8 ? 'high' : 'normal';
enqueue($queue, $priority, $ticket_text);

Batch decisions about one state

my %questions = (
    refund_requested => noul(instructions => 'Is a refund requested?'),
    contains_pii     => noul(instructions => 'Does the message contain PII?'),
    sentiment        => choice(
        instructions => 'What is the dominant sentiment?',
        criteria => { positive => undef, neutral => undef, negative => undef },
    ),
    severity => score(
        instructions => 'How severe is the reported problem?',
        criteria => ['Minor', 'Moderate', 'Major', 'Critical'],
    ),
);

my $result = $client->system_one(
    state => $message,
    questions => \%questions,
);

Question helpers are optional; raw hashes also work:

questions => {
    urgent => { type => 'noul', instructions => 'Is this urgent?' },
}

15. Troubleshooting

Can't locate TypeSafe/SDK.pm in @INC

The SDK is not installed or Perl cannot see it. From the unpacked project, run perl -Ilib your_script.pl. Check PERL5LIB after a user-local installation.

TypeSafe API key is required

Pass a nonempty api_key to WebService::TypeSafe->new. If you intentionally use the environment fallback, set TYPESAFE_API_KEY in the same shell that starts Perl. Check its presence without printing the secret:

perl -e 'print $ENV{TYPESAFE_API_KEY} ? "key is set\n" : "key is missing\n"'

HTTP 401

The key is invalid or expired. Update it and restart the process so it receives the new environment.

HTTP 422

Check question types and criteria. Choice needs a nonempty hash; Score needs 2–10 levels. The exception's body contains server validation details.

HTTP 429 or 529

The service is rate-limited or overloaded. Default retries handle these statuses. If failures continue, reduce concurrency or increase the retry budget.

Uncertain results

Make instructions and criteria more distinct, include the relevant state, and route low-confidence answers for human review.

Further help

After installation, run perldoc WebService::TypeSafe. TypeSafe's public documentation is at https://docs.typesafe.ai/.