jev

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 22, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

README

go-jev

Go Reference CI Go Report Card License

A Go client for the Jev judgement API.

Give it a piece of state and a set of questions; it returns one structured answer per question, keyed the way you asked them.

Install

go get github.com/leonardjke/go-jev

Requires Go 1.24 or later.

Quickstart

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/leonardjke/go-jev"
)

func main() {
	client, err := jev.New(os.Getenv("JEV_API_KEY"))
	if err != nil {
		log.Fatal(err)
	}

	ticket := "Hi -- I was charged twice for June and nobody has replied in a week."

	resp, err := client.Request(context.Background(), ticket,
		jev.Choice("department", "Which team should handle this?", map[string]string{
			"billing":   "Payment or subscription issues",
			"technical": "Bugs or integration problems",
		}),
		jev.Score("frustration", "How frustrated does the customer appear?",
			"Calm", "Frustrated but civil", "Very angry"),
		jev.Noul("is_urgent", "The message conveys urgency"),
	)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(resp.Answers["department"].Choice)
}

state is the text being judged. Every question carries a key of its own, and the answers come back under those keys in resp.Answers.

Question types

Constructor Asks for Answer field
jev.Noul(key, question) a yes/no reading Answer.Noul
jev.Choice(key, question, criteria) one key out of criteria Answer.Choice
jev.Score(key, question, scale...) one point on an ordered scale Answer.Score

Noul is a plain check. Criteria are optional — add them with WithCriteria when what counts as true needs spelling out.

jev.Noul("is_urgent", "The message conveys urgency")

jev.Noul("is_spam", "The message is spam").
	WithCriteria(map[string]string{
		"true":  "Unsolicited bulk advertising",
		"false": "A genuine request, even a badly written one",
	})

Choice maps each possible answer to the description that qualifies it. The winning key comes back in Answer.Choice.

jev.Choice("department", "Which team should handle this?", map[string]string{
	"billing":   "Payment or subscription issues",
	"technical": "Bugs or integration problems",
})

Score takes scale labels lowest to highest. Answer.Score is the position picked, and Answer.Legend maps positions back to their labels.

jev.Score("frustration", "How frustrated does the customer appear?",
	"Calm", "Frustrated but civil", "Very angry")

Reading answers

a := resp.Answers["frustration"]

if a.Score != nil {
	fmt.Println(*a.Score, a.Legend) // the position, and the labels it maps to
}
if a.Confidence != nil {
	fmt.Println(*a.Confidence)
}
fmt.Println(a.Probabilities) // the spread the answer was picked from
fmt.Println(resp.Usage.InputTokens, resp.Usage.OutputTokens)

Score, Noul and Confidence are pointers because 0 is a meaningful value for each of them and has to be told apart from "not returned".

Question context

Questions that need more than their text take it through chained With* calls. The ones that don't stay a single line.

resp, err := client.Request(ctx, invoice,
	jev.Noul("invoice_number_matches",
		"Does `extracted_value` match the `field` as it appears in `source_text`?").
		WithField(jev.Field{
			Name:        "invoice_number",
			Type:        "string",
			Description: "The identifier printed on the invoice.",
		}).
		WithExtractedValue("4471"),

	jev.Score("amount_due_size", "How large is the `field` value in `source_text`?",
		"Small", "Typical", "Unusually large").
		WithField(jev.Field{Name: "amount_due", Type: "number", Unit: "USD"}),

	jev.Noul("sender_mismatch", "Does the claimed sender identity conflict with the sending domain?").
		WithCompare("ticket.sender.display_name", "ticket.sender.email").
		WithFocus("Compare the named organization with the email domain."),

	jev.Score("pr_focus", "How focused is this pull request on a single change?",
		"One change", "A few related changes", "Unrelated changes").
		WithNote("Judge the number of independent changes, not the size of any one change."),
)
Method Instruction key Holds
WithField field the value being judged
WithExtractedValue extracted_value the candidate to check the field against
WithCompare compare state paths to weigh against each other
WithFocus focus what the judge should look at
WithNote note a caveat for how to judge
WithCriteria what makes a Noul answer true or false

Set(key, value) is the escape hatch for any instruction key without a typed helper yet:

jev.Noul("within_tolerance", "Is `extracted_value` within tolerance of the `field`?").
	WithExtractedValue(4471).
	Set("tolerance", map[string]float64{"percent": 0.5})

Unrecognized keys also survive a decode and re-encode untouched, so a request read off the wire round-trips.

Client options

client, err := jev.New(apiKey,
	jev.WithModel("jev-latest"),
	jev.WithEndpoint("https://gateway.internal/v1/systemone"),
	jev.WithClient(&http.Client{Timeout: 10 * time.Second}),
	jev.WithLogger(slog.Default()),
)
Option Default Notes
WithModel jev-latest model to judge with
WithEndpoint https://api.typesafe.ai/v1/systemone full URL the request is posted to — nothing is appended, so a proxy or compatible service needs its whole path
WithClient &http.Client{Timeout: 30 * time.Second} a nil client is ignored rather than applied
WithLogger discards output any Debug(msg string, args ...any), so *slog.Logger fits as-is; a nil logger is rejected by New

Responses are read through a limit of 8 MiB.

API keys stay out of logs

The key is held as a type that redacts itself through fmt, String(), JSON and encoding.TextMarshaler, for every verb rather than just the string ones. Printing a client yields the endpoint and model and never the credential:

fmt.Println(client) // Client{endpoint: "https://api.typesafe.ai/v1/systemone", model: "jev-latest"}

Reaching the real value takes an explicit string() conversion, which is the one place a caller has to mean it.

Errors

Non-2xx responses come back as *jev.APIError:

resp, err := client.Request(ctx, state, questions...)

var apiErr *jev.APIError
switch {
case errors.As(err, &apiErr):
	log.Printf("status %d: %s", apiErr.StatusCode, apiErr.Body)
case err != nil:
	log.Fatal(err)
}

Request assembly is validated before anything is sent. An empty question key, a duplicate key, no questions at all, or a failed Set value each return an error naming the question at fault.

The data package

github.com/leonardjke/go-jev/data holds the request and response types directly and is usable on its own — for a server implementing this API, or for tests that build a payload by hand. data.Question, data.Instructions, data.Response and friends live there; the constructors in the root package are a layer on top.

Criteria decode into the concrete type matching the question type, so you get a typed value back rather than an any to assert on:

scale, ok := data.QuestionCriteria[data.CriteriaScore](q)    // "score"
answers, ok := data.QuestionCriteria[data.CriteriaOthers](q) // "noul" / "choice"

Testing

go test ./...

End-to-end tests are behind the e2e build tag and talk to the live API. They skip unless API_KEY is set — copy .env.example to .env and fill it in:

API_KEY=... API_URL=... go test -tags e2e -v ./...

License

Apache 2.0 — see LICENSE and NOTICE.

Documentation

Overview

Package jev is a client for the Jev judgement API.

Give it a piece of state and a set of questions; it returns one structured answer per question, keyed the way you asked them.

client, err := jev.New(os.Getenv("JEV_API_KEY"))
if err != nil {
	log.Fatal(err)
}

resp, err := client.Request(ctx, ticket,
	jev.Choice("department", "Which team should handle this?", map[string]string{
		"billing":   "Payment or subscription issues",
		"technical": "Bugs or integration problems",
	}),
	jev.Score("frustration", "How frustrated does the customer appear?",
		"Calm", "Frustrated but civil", "Very angry"),
	jev.Noul("is_urgent", "The message conveys urgency"),
)

Questions

Noul asks a yes/no question, Choice picks one key out of a set of described answers, and Score picks one point on an ordered scale. Those cover the common case on their own; the With* methods on Question add instruction context to the questions that need it and leave the rest alone. Question.Set is the escape hatch for context keys with no typed helper yet, and unrecognized keys survive a decode and re-encode untouched.

Answers

Answers come back under the keys they were asked with, in github.com/leonardjke/go-jev/data.Response.Answers, alongside the confidence, the probability spread the answer was drawn from and, for a score, the legend mapping positions back to labels. Score, Noul and Confidence are pointers, so a returned 0 is distinguishable from "not returned".

Configuration

New takes the API key plus any number of Option values: WithModel, WithEndpoint, WithClient and WithLogger. The key is held in a type that redacts itself through fmt, JSON and encoding.TextMarshaler, so printing a client or logging it never discloses the credential.

Non-2xx responses come back as *APIError, matchable with errors.As.

Example (Context)

Example_context adds instruction context to the questions that need it. The call is the same one as above — only the questions carrying extra context grow, and the plain ones stay plain.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/leonardjke/go-jev"
)

func main() {
	client, err := jev.New("...")
	if err != nil {
		log.Fatal(err)
	}

	resp, err := client.Request(context.Background(), invoice,
		jev.Noul("invoice_number_matches",
			"Does `extracted_value` match the `field` as it appears in `source_text`?").
			WithField(jev.Field{
				Name:        "invoice_number",
				Type:        "string",
				Description: "The identifier printed on the invoice.",
			}).
			WithExtractedValue("4471"),

		jev.Score("amount_due_size", "How large is the `field` value in `source_text`?",
			"Small", "Typical", "Unusually large").
			WithField(jev.Field{Name: "amount_due", Type: "number", Unit: "USD"}),

		jev.Noul("sender_mismatch", "Does the claimed sender identity conflict with the sending domain?").
			WithCompare("ticket.sender.display_name", "ticket.sender.email").
			WithFocus("Compare the named organization with the email domain."),

		// Set is the escape hatch for instruction keys with no typed helper.
		jev.Noul("within_tolerance", "Is `extracted_value` within tolerance of the `field`?").
			WithExtractedValue(4471).
			Set("tolerance", map[string]float64{"percent": 0.5}),
	)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(resp.Answers["invoice_number_matches"].Noul)
}

var invoice = "..."
Example (Simple)

Example_simple is the everyday shape: build the client once, then ask. Questions that need no context are a single call each.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/leonardjke/go-jev"
)

func main() {
	client, err := jev.New("...")
	if err != nil {
		log.Fatal(err)
	}

	resp, err := client.Request(context.Background(), ticket,
		jev.Choice("department", "Which team should handle this?", map[string]string{
			"billing":   "Payment or subscription issues",
			"technical": "Bugs or integration problems",
		}),
		jev.Score("frustration", "How frustrated does the customer appear?",
			"Calm", "Frustrated but civil", "Very angry"),
		jev.Noul("is_urgent", "The message conveys urgency"),
	)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(resp.Answers["department"].Choice)
}

var ticket = "..."

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type APIError

type APIError struct {
	StatusCode int
	Body       string
}

func (*APIError) Error

func (e *APIError) Error() string

type Field

type Field = data.Field

Field describes the value a question is asked about.

type Jev

type Jev struct {
	// contains filtered or unexported fields
}

func New

func New(apiKey string, opts ...Option) (*Jev, error)

func (Jev) Format

func (j Jev) Format(f fmt.State, _ rune)

Format keeps the client printable without printing the credential. The receiver is a value so that a copied Jev is covered too, rather than falling back to reflection over its fields.

func (*Jev) Request

func (j *Jev) Request(ctx context.Context, state string, questions ...Question) (*data.Response, error)

Request judges state against questions, using the model the client was built with.

resp, err := client.Request(ctx, ticket,
	jev.Choice("department", "Which team should handle this?", map[string]string{
		"billing":   "Payment or subscription issues",
		"technical": "Bugs or integration problems",
	}),
	jev.Score("frustration", "How frustrated is the customer?",
		"Calm", "Frustrated but civil", "Very angry"),
)

Every question needs a key of its own; the answers come back under those keys in Response.Answers.

type Logger

type Logger interface {
	Debug(msg string, args ...any)
}

type Option

type Option func(j *Jev)

func WithClient

func WithClient(c *http.Client) Option

func WithEndpoint

func WithEndpoint(endpoint string) Option

WithEndpoint points the client at another URL: a proxy, a gateway or a compatible service. It is the full endpoint the request is posted to, path included, not just a host -- nothing is appended to it.

func WithLogger

func WithLogger(l Logger) Option

WithLogger sends the client's debug output to l. The default logger discards it. A nil logger is rejected by New rather than ignored.

func WithModel

func WithModel(model string) Option

type Question

type Question struct {
	// contains filtered or unexported fields
}

Question is one question to ask, along with the key its answer comes back under in Response.Answers.

Build one with Noul, Choice or Score. Those cover the common case on their own; the With* methods add context to the questions that need it, and leave the ones that don't alone:

jev.Noul("is_urgent", "The message conveys urgency")

jev.Score("amount_due_size", "How large is the `field` value in `source_text`?",
	"Small", "Typical", "Unusually large").
	WithField(jev.Field{Name: "amount_due", Type: "number", Unit: "USD"})

The zero Question is not usable; Request rejects one with an empty key.

func Choice

func Choice(key, question string, criteria map[string]string) Question

Choice asks the judge to pick one key out of criteria, which maps each possible answer to the description that qualifies it.

func Noul

func Noul(key, question string) Question

Noul asks a yes/no question. Criteria are optional — a plain check needs none, WithCriteria describes what makes each answer true or false.

func Score

func Score(key, question string, scale ...string) Question

Score asks the judge to pick one point on an ordered scale. The scale labels run lowest to highest, e.g. "Calm", "Frustrated", "Very angry".

func (Question) Key

func (q Question) Key() string

Key returns the key this question's answer comes back under.

func (Question) Set

func (q Question) Set(key string, value any) Question

Set stores value under an arbitrary instruction key. It is the escape hatch for context keys this library has no typed helper for yet.

func (Question) Validate

func (q Question) Validate() error

func (Question) WithCompare

func (q Question) WithCompare(paths ...string) Question

WithCompare sets "compare": the state paths the question weighs against each other, e.g. "ticket.sender.display_name".

func (Question) WithCriteria

func (q Question) WithCriteria(criteria map[string]string) Question

WithCriteria describes what makes each answer of a Noul question true or false. It replaces any criteria already set.

func (Question) WithExtractedValue

func (q Question) WithExtractedValue(value any) Question

WithExtractedValue sets "extracted_value": the candidate value to check the field against.

func (Question) WithField

func (q Question) WithField(f Field) Question

WithField sets "field": the value being judged.

func (Question) WithFocus

func (q Question) WithFocus(focus string) Question

WithFocus sets "focus": what the judge should look at.

func (Question) WithNote

func (q Question) WithNote(note string) Question

WithNote sets "note": a caveat for how to judge.

Directories

Path Synopsis
internal
log
privacy
Package privacy holds the types that keep credentials out of logs, error messages and anything else that formats or marshals a value.
Package privacy holds the types that keep credentials out of logs, error messages and anything else that formats or marshals a value.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL