hyf

Context-aware query service for Radroots
git clone https://radroots.dev/git/hyf.git
Log | Files | Refs | README | LICENSE

commit d04a0ddf77b6e96f107bcd174df46d2df929d25b
parent d03bebf7ce20a4390d84c47819b202633b9ab799
Author: triesap <tyson@radroots.org>
Date:   Mon, 21 Sep 2026 13:05:36 +0000

provider: validate answer sets and numerical consistency

Diffstat:
Msrc/hyf_provider/jev_answers.mojo | 35+++++++++++++++++++++++++++++++++++
Mtests/test_jev.mojo | 36++++++++++++++++++++++++++++++++++++
2 files changed, 71 insertions(+), 0 deletions(-)

diff --git a/src/hyf_provider/jev_answers.mojo b/src/hyf_provider/jev_answers.mojo @@ -104,3 +104,38 @@ def parse_score_answer( if total < 0.999 or total > 1.001: raise Error("provider_answer_bad_distribution_sum") return typed_score(question_id, score, len(rubric), confidence) + + +from hyf_assist.questions import QuestionBundle + + +def parse_jev_response( + body: Value, bundle: QuestionBundle +) raises -> List[TypedAnswer]: + if not body.is_object(): + raise Error("provider_response_invalid") + if not _has_key(body, "model") or not _has_key(body, "answers"): + raise Error("provider_response_invalid") + if body["model"].string_value() != bundle.model: + raise Error("provider_model_mismatch") + var answers = body["answers"] + if not answers.is_object(): + raise Error("provider_response_invalid") + + var result = List[TypedAnswer]() + for question in bundle.questions: + if not _has_key(answers, question.id): + raise Error("provider_answer_missing") + var answer = answers[question.id] + if question.kind == "choice": + result.append(parse_choice_answer(question.id, answer, question.choices)) + elif question.kind == "noul": + result.append(parse_noul_answer(question.id, answer)) + elif question.kind == "score": + result.append(parse_score_answer(question.id, answer, question.rubric)) + else: + raise Error("provider_answer_wrong_type") + + if len(answers.object_keys()) != len(bundle.questions): + raise Error("provider_answer_extra") + return result^ diff --git a/tests/test_jev.mojo b/tests/test_jev.mojo @@ -108,3 +108,39 @@ def test_parse_score_answer_validates_rubric_and_levels() raises: loads('{"type":"score","score":1,"legend":{"0":"a"},"confidence":1.0}'), rubric, ) + + +from hyf_provider.jev_answers import parse_jev_response + + +def test_parse_jev_response_validates_answer_set() raises: + var body = loads( + '{"model":"jev-1.13.0","answers":{' + '"supply_status":{"type":"choice","choice":"offered","probabilities":{"offered":1.0,"forecast":0.0,"unclear":0.0},"confidence":1.0},' + '"seconds_ok":{"type":"noul","noul":0.9},' + '"culinary_fit":{"type":"score","score":2,"legend":{"0":"u","1":"l","2":"s"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"confidence":1.0}},' + '"usage":{"input_tokens":10,"output_tokens":5}}' + ) + var answers = parse_jev_response(body, _bundle()) + assert_equal(len(answers), 3) + + var extra = loads( + '{"model":"jev-1.13.0","answers":{' + '"supply_status":{"type":"choice","choice":"offered","probabilities":{"offered":1.0,"forecast":0.0,"unclear":0.0},"confidence":1.0},' + '"seconds_ok":{"type":"noul","noul":0.9},' + '"culinary_fit":{"type":"score","score":2,"legend":{"0":"u","1":"l","2":"s"},"probabilities":{"0":0.0,"1":0.0,"2":1.0},"confidence":1.0},' + '"extra":{"type":"noul","noul":0.5}},' + '"usage":{"input_tokens":10,"output_tokens":5}}' + ) + with assert_raises(): + _ = parse_jev_response(extra, _bundle()) + + var mismatch = loads( + '{"model":"jev-other","answers":{"supply_status":{"type":"choice","choice":"offered","confidence":1.0},"seconds_ok":{"type":"noul","noul":0.9},"culinary_fit":{"type":"score","score":2,"confidence":1.0}}}' + ) + with assert_raises(): + _ = parse_jev_response(mismatch, _bundle()) + + var missing = loads('{"model":"jev-1.13.0","answers":{"supply_status":{"type":"choice","choice":"offered","confidence":1.0}}}') + with assert_raises(): + _ = parse_jev_response(missing, _bundle())