Skip to content

Add runnable examples, each smoke-tested against a stub - #21

Merged
garretpremo merged 2 commits into
mainfrom
feat/examples
Oct 15, 2026
Merged

garretpremo merged 2 commits into
mainfrom
feat/examples

Conversation

@garretpremo

Copy link
Copy Markdown
Contributor

Refs #20. The issue was closed as not needed, but in-repo examples still help the repo: CI now breaks when an SDK change breaks a realistic program, newcomers have something they can run in one command, and the JS SDK has an examples/ directory too.

What's here

Four examples under examples/. Each is its own Gradle project with a short README, a run task, and a smoke test.

Example Shows
ticket-triage Keyed questions, models listing, and API errors; a port of the JS SDK's demo.ts
typed-routing Ask handles and an enum Choice, routed to a queue by an exhaustive switch
async-batch systemOneAsync fan-out with per-call options, a custom retry policy, and failures per item
spring-boot The starter behind POST /tickets/route, with TypeSafe failures mapped to 502/504 ProblemDetail
TYPESAFE_API_KEY=your-key ./gradlew :examples:ticket-triage:run
  • Testing: each example takes its client through run(TypeSafeClient), so its test points it at examples/support's in-process StubTypeSafeServer. The Spring test sets typesafe.base-url instead. ./gradlew build checks all four with no API key and no secrets in CI.
  • README samples: these are output from real runs against the API.
  • Publishing: the root build's shared block now configures only typesafe-sdk and typesafe-sdk-spring-boot-starter, so nothing under examples/ is published. A dry run of publishToMavenCentral touches only those two modules.
  • Java level: the examples target Java 17, the same as the SDK.

Follow-ups found while writing these (not in this PR)

  • Root README, Spring Boot section: typesafe.api-key=${TYPESAFE_API_KEY} doesn't fail fast when the variable is unset. Boot binds the literal ${TYPESAFE_API_KEY} as the key, so the first request gets a 401. ${TYPESAFE_API_KEY:}, as the starter's Javadoc describes, is the form that works.
  • Root README, Async section: an exceptionally callback receives a CompletionException wrapping the TypeSafeException, not the exception itself. Only get()/join() hand back the unwrapped cause.
  • Possible API additions: ChoiceAnswer.probability() for the chosen label, and a systemOne(state, RequestOptions, Ask...) overload so per-call options don't force the builder form.

Four examples under examples/, each its own Gradle project with a
README, a run task, and a smoke test:

- ticket-triage: keyed questions, models listing, and API errors; a
  port of the JavaScript SDK's demo.
- typed-routing: Ask handles and an enum Choice, routed to a queue by
  an exhaustive switch.
- async-batch: systemOneAsync fan-out with per-call options, a custom
  retry policy, and failures per item that don't sink the batch.
- spring-boot: the starter behind POST /tickets/route, with TypeSafe
  failures mapped to 502 or 504 ProblemDetail responses.

Each example takes its client through run(TypeSafeClient), so the tests
point it at examples/support's in-process stub, and ./gradlew build
checks every example with no API key. The README samples are output
from real runs.

The root build now configures only the two library modules, so nothing
under examples/ is published.

Refs #20
@garretpremo garretpremo mentioned this pull request Oct 5, 2026
}

static String level(ScoreAnswer s) {
return "%.2f (%s)".formatted(s.score(), s.legend().get(String.valueOf(Math.round(s.score()))));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want the argmax on the probabilities map to inform the level, rather than the aggregated ScoreAnswer score. Most likely level is the mode vs mean, in other words?

Suggested change
return "%.2f (%s)".formatted(s.score(), s.legend().get(String.valueOf(Math.round(s.score()))));
String likeliest = s.probabilities().entrySet().stream().max(Map.Entry.comparingByValue()).orElseThrow().getKey();
return "%.2f (%s)".formatted(s.score(), s.legend().get(likeliest));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, nice catch. The rounded mean can pick a level that isn't the model's most likely one.

The test fixture shows this: refund_risk has {0: 0.1, 1: 0.4, 2: 0.5}, so the mean is 1.4 and rounds to level 1, though level 2 is the most likely.

AsyncBatch rounded the same way, so I fixed it too and updated the README output.

score() is the probability-weighted mean, so rounding it can name a level
the model doesn't favour: refund_risk at 1.4 with {0: 0.1, 1: 0.4, 2: 0.5}
rounded to "possible" when "likely" carries the most probability. Take the
argmax of probabilities instead, in both TicketTriage and AsyncBatch, and
refresh the async-batch sample output, where one row's label changed.
@garretpremo
garretpremo merged commit 12c097f into main Oct 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants