Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- `ScoreAnswer.legend()` is now `Map<String, Object>`, since levels are sent as any JSON value and echoed back as sent. A `Score` with an object or array level previously failed the whole response, including its other answers and usage, with `Cannot deserialize value of type java.lang.String` (#11).

## 0.4.0 - 2026-09-22

- Logging through slf4j on the `io.github.premocloud.typesafe` logger: `DEBUG` for one line per request with status and elapsed, plus retries and connection failures; `TRACE` for the wire in both directions. Nothing at `INFO` or above. Credential headers are masked. Adds `org.slf4j:slf4j-api` (#6, #7).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
* @param score probability-weighted position from 0 to the top level index
* @param probabilities probability per level index (keys are the index as a string)
* @param confidence 0 to 1, how concentrated the distribution is
* @param legend level index back to the description that was sent
* @param legend level index back to the description that was sent: a String, or the Map or List it was given as
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record ScoreAnswer(
double score,
Map<String, Double> probabilities,
double confidence,
Map<String, String> legend
Map<String, Object> legend
) implements TypeSafeAnswer {

/** Jackson entry point: a score answer missing its score, probabilities, or confidence is malformed. */
Expand All @@ -26,7 +26,7 @@ public record ScoreAnswer(
@JsonProperty("score") Double score,
@JsonProperty("probabilities") Map<String, Double> probabilities,
@JsonProperty("confidence") Double confidence,
@JsonProperty("legend") Map<String, String> legend,
@JsonProperty("legend") Map<String, Object> legend,
@JsonProperty("type") String ignoredType
) {
this(TypeSafeAnswer.required(score, "score", "score").doubleValue(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,28 @@ void systemOneRejectsScoreAnswerMissingItsScore() {
assertTrue(exception.getMessage().contains("score"), exception.getMessage());
}

@Test
void systemOneReadsAScoreLegendWithObjectAndArrayLevels() {
// Levels are sent as any JSON value and echoed back as sent, so the legend holds whatever was asked (#11).
server.reply(200, """
{"model": "jev-1.13.0", "answers": {
"is_phishing": {"type": "noul", "noul": 0.93},
"spam_category": {"type": "choice", "choice": "PHISHING", "probabilities": {"PHISHING": 1.0}, "confidence": 1.0},
"urgency": {"type": "score", "score": 1.0, "probabilities": {"0": 0.2, "1": 0.5, "2": 0.3}, "confidence": 0.5,
"legend": {"0": "none", "1": {"what": "Threatens loss within hours", "examples": ["final notice"]}, "2": ["a", "b"]}}},
"usage": {"input_tokens": 1, "output_tokens": 1}}
""");

TypeSafeResponse response = client.systemOne(spamRequest());

Map<String, Object> legend = response.score("urgency").legend();
assertEquals("none", legend.get("0"));
assertEquals(Map.of("what", "Threatens loss within hours", "examples", List.of("final notice")), legend.get("1"));
assertEquals(List.of("a", "b"), legend.get("2"));
assertEquals(0.93, response.noul("is_phishing"));
assertEquals(1, response.usage().inputTokens());
}

@Test
void systemOneRejectsAResponseMissingAnAnswerForAQuestionThatWasAsked() {
server.reply(200, """
Expand Down
Loading