Documentation
¶
Overview ¶
Package jev is a type-safe Go client for TypeSafe's System One API and its model Jev.
Jev reads natural language and structured state like an LLM, but instead of generating text it answers questions whose possible answers you define in advance, with a probability for each. This package turns those questions into Go types, so the answer to a Choice over your own enum comes back as that enum.
Asking one question ¶
type Intent string
const (
Refund Intent = "refund"
Shipping Intent = "shipping"
Other Intent = "other"
)
var intentQ = jev.Choice("What does the customer want?",
jev.Opt(Refund, "wants money back"),
jev.Opt(Shipping, "asks where the order is"),
jev.Opt(Other, "anything else"),
)
ans, err := jev.Ask(ctx, client, ticket, intentQ)
if err != nil {
return err
}
if intent, sure := ans.Sure(0.9); sure && intent == Refund {
return autoRefund(ticket)
}
Asking many questions in one call ¶
Independent questions about the same state belong in one Batch. They are evaluated in parallel and cost one request. Each Add returns a typed Handle:
b := client.Batch(review)
spam := jev.Add(b, jev.Noul("Is this review spam or advertising?"))
intent := jev.Add(b, intentQ)
if _, err := b.Run(ctx); err != nil {
return err
}
s, err := spam.Get() // jev.NoulAnswer
i, err := intent.Get() // jev.ChoiceAnswer[Intent]
Questions can carry structure ¶
Instructions, option descriptions, score levels and noul criteria are all Content: a string, or any value that marshals to JSON. Structure sharpens a boundary the model keeps getting wrong, and lets a taxonomy or a schema be passed through instead of flattened into a sentence:
jev.Choice("Which department does this product belong to?",
jev.Opt(Sporting, map[string]any{
"Cycling": []string{"Bike Bottles & Cages", "Helmets"},
"Fitness": []string{"Yoga Mats"},
}),
jev.Opt(Drinkware, map[string]any{
"what": "Everyday water bottles, travel mugs, tumblers",
"not_for": "Sport-specific gear",
}),
)
When the API moves first ¶
RawQuestion sends a Spec exactly as given and returns the answer undecoded, so a question kind or field that lands before a release of this package does is still reachable. Spec.Extra carries unmodelled fields.
Policy stays in your code ¶
Low confidence is not an error. Answers carry the full distribution, and the thresholds that decide what to do with it belong to the caller. Errors are reserved for invalid questions, transport and API failures, and answers that do not fit the question.
Typed output guarantees the shape of an answer, not its truth. Validate your thresholds on labelled data before trusting them in production.
Configuration ¶
New reads TYPESAFE_API_KEY, TYPESAFE_BASE_URL and TYPESAFE_DEFAULT_MODEL when the matching option is not given. Callers whose configuration comes from somewhere else pass the key directly:
client, err := jev.NewWithKey(cfg.TypeSafeKey)
An explicitly supplied key disables the environment fallback entirely, so an empty one is ErrNoAPIKey rather than a silent fall back.
Testing ¶
Package github.com/anilsenay/jev/jevtest provides a rule-based fake Provider, so code that branches on Jev can be tested without the network.
Index ¶
- Constants
- Variables
- func Ask[A any](ctx context.Context, c *Client, state any, q Question[A]) (A, error)
- func Text(c Content) string
- type APIError
- type Batch
- type Cache
- type ChoiceAnswer
- type ChoiceOption
- type Client
- type Content
- type FieldError
- type Handle
- type Kind
- type MemoryCache
- type Meta
- type Middleware
- type ModelCard
- type ModelLister
- type NoulAnswer
- type NoulCriteria
- type Option
- func WithAPIKey(key string) Option
- func WithBaseURL(url string) Option
- func WithCache(cache Cache) Option
- func WithHTTPClient(hc *http.Client) Option
- func WithHeaders(h map[string]string) Option
- func WithLogger(l *slog.Logger) Option
- func WithMiddleware(m ...Middleware) Option
- func WithModel(model string) Option
- func WithProvider(p Provider) Option
- func WithRetries(n int) Option
- func WithRetry(r Retry) Option
- func WithTimeout(d time.Duration) Option
- func WithUserAgent(ua string) Option
- type Provider
- type ProviderFunc
- type Question
- func Choice[T ~string](instructions Content, options ...ChoiceOption[T]) Question[ChoiceAnswer[T]]
- func Noul(instructions Content, criteria ...NoulCriteria) Question[NoulAnswer]
- func OneOf[T ~string](instructions Content, values ...T) Question[ChoiceAnswer[T]]
- func RawQuestion(spec Spec) Question[RawAnswer]
- func Score(instructions Content, levels ...Content) Question[ScoreAnswer]
- type RawAnswer
- type Request
- type Response
- type Retry
- type ScoreAnswer
- type Spec
- type Usage
Constants ¶
const ( DefaultBaseURL = "https://api.typesafe.ai" DefaultModel = "jev-latest" DefaultTimeout = 10 * time.Second // APIKeyEnv is read when no key is passed with WithAPIKey. APIKeyEnv = "TYPESAFE_API_KEY" // BaseURLEnv is read when no URL is passed with WithBaseURL. BaseURLEnv = "TYPESAFE_BASE_URL" // DefaultModelEnv is read when no model is passed with WithModel. DefaultModelEnv = "TYPESAFE_DEFAULT_MODEL" )
Defaults used when an option is not supplied. They match the official TypeSafe SDKs.
const ( // MaxChoiceOptions is the largest number of options one choice may offer. MaxChoiceOptions = 255 // MaxScoreLevels is the largest number of levels one score may define. MaxScoreLevels = 10 )
Limits the API enforces on a single question. Like the official SDKs, this package does not check them before sending: a question that exceeds one comes back as an APIError matching ErrBadRequest. They are exported so callers building option lists programmatically can check for themselves.
const (
// StatusOverloaded is the non-standard status the API returns when overloaded.
StatusOverloaded = 529
)
const Version = "0.1.0"
Version is the package version, sent in the User-Agent and X-TypeSafe-SDK headers.
Variables ¶
var ( // ErrNoAPIKey means no key was supplied and TYPESAFE_API_KEY was unset. ErrNoAPIKey = errors.New("jev: no API key: set TYPESAFE_API_KEY or use WithAPIKey") // ErrInvalidQuestion means a question was built with invalid arguments, // for example a score with one level. It is detected before any request. ErrInvalidQuestion = errors.New("jev: invalid question") // ErrMalformedAnswer means the provider returned an answer that does not fit // its question: a missing answer, the wrong kind, or an option that was // never offered. It is the counterpart of the official SDKs' response // validation error. ErrMalformedAnswer = errors.New("jev: malformed answer") // ErrNotRun means a Handle was read before its Batch ran. ErrNotRun = errors.New("jev: batch has not run") // ErrBatchUsed means a Batch was run twice or added to after running. ErrBatchUsed = errors.New("jev: batch already run") // ErrNoModelList means the client's provider cannot list models. ErrNoModelList = errors.New("jev: provider does not list models") )
Errors raised before a request is sent.
var ( // ErrConnection means the request never produced an HTTP response: DNS, TLS, // a refused connection, or a body that stopped mid-flight. ErrConnection = errors.New("jev: connection error") // ErrTimeout means an attempt exceeded its timeout. It also matches // ErrConnection, as it does in the official SDKs. ErrTimeout = errors.New("jev: request timed out") )
Errors raised by the transport, below HTTP. They mirror the official SDKs' APIConnectionError and APITimeoutError.
var ( // ErrBadRequest is 400: the API rejected the request. ErrBadRequest = errors.New("jev: bad request") // ErrAuth is 401: the API key is missing, wrong or revoked. ErrAuth = errors.New("jev: authentication failed") // ErrPermissionDenied is 403: the key is valid but not allowed to do this. ErrPermissionDenied = errors.New("jev: permission denied") // ErrNotFound is 404. ErrNotFound = errors.New("jev: not found") // ErrUnprocessable is 422: the request body failed server validation. ErrUnprocessable = errors.New("jev: unprocessable entity") // ErrInvalidRequest matches both 400 and 422. ErrInvalidRequest = errors.New("jev: invalid request") // ErrRateLimit is 429. ErrRateLimit = errors.New("jev: rate limited") // ErrOverloaded is 529, the status TypeSafe returns when it is overloaded. ErrOverloaded = errors.New("jev: overloaded") // ErrInternalServer is any 5xx. ErrInternalServer = errors.New("jev: internal server error") )
Errors matching an API status, one per status the official SDKs distinguish. ErrInvalidRequest is an umbrella over both request-validation statuses.
var DefaultRetry = Retry{ MaxRetries: 2, BackoffInitial: 500 * time.Millisecond, BackoffMax: 5 * time.Second, BackoffJitter: 0.25, RespectRetryAfter: true, MaxRetryAfter: time.Minute, RetryConnection: true, RetryTimeout: true, }
DefaultRetry is the policy used when none is supplied.
Functions ¶
Types ¶
type APIError ¶
type APIError struct {
// Status is the HTTP status code.
Status int
// Body is the response body, truncated to a readable length.
Body string
// Header is the response header, for anything this type does not model.
Header http.Header
// RequestID is the x-typesafe-request-id header. Quote it in a bug report.
RequestID string
// RetryAfter is the delay the server asked for, from retry-after-ms or
// Retry-After. It is zero when the response carried neither.
RetryAfter time.Duration
// Endpoint is the method and path of the request, for context.
Endpoint string
// Type is the API's error_type, such as "authentication_error" or
// "api_usage_error". It is empty when the body carried none.
Type string
// Message is the API's human-readable explanation, extracted from the body.
Message string
// Fields lists the offending fields of a validation failure.
Fields []FieldError
}
APIError is a non-2xx response from the API. errors.Is matches it against the sentinel for its status.
type Batch ¶
type Batch struct {
// contains filtered or unexported fields
}
Batch collects questions about one state and evaluates them in a single request. Build it with Client.Batch, add questions with Add, then call Batch.Run once. A Batch is not safe for concurrent use.
type Cache ¶
Cache stores responses by request key. Implementations must be safe for concurrent use and must treat stored responses as read-only.
type ChoiceAnswer ¶
type ChoiceAnswer[T ~string] struct { // Value is the most probable option. Value T // Probs holds a probability for every option, including zero ones. Probs map[T]float64 // Confidence is the API's confidence: how peaked the distribution is, from // 0 to 1. It is not a calibrated probability that the answer is right; use // Probs[Value] for that. Confidence float64 }
ChoiceAnswer is the selected option and the distribution over all options.
func (ChoiceAnswer[T]) Ranked ¶
func (a ChoiceAnswer[T]) Ranked() []T
Ranked returns the options from most to least probable.
func (ChoiceAnswer[T]) Sure ¶
func (a ChoiceAnswer[T]) Sure(threshold float64) (T, bool)
Sure returns Value and whether its probability is at least threshold.
type ChoiceOption ¶
ChoiceOption is one choice option with an optional description. The description may be a string, structured Content, or nil.
func Opt ¶
func Opt[T ~string](value T, description Content) ChoiceOption[T]
Opt builds a ChoiceOption. T is inferred from value, so passing your own string enum makes the whole choice typed. Pass nil as the description when the option name speaks for itself.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client evaluates questions through a Provider. It is safe for concurrent use.
func New ¶
New builds a client. Without WithProvider it talks to the TypeSafe API and returns ErrNoAPIKey when no key is available.
Explicit options win over environment variables, which win over the defaults.
func NewWithKey ¶
NewWithKey is New with the API key supplied directly, for callers that read their configuration through something other than the process environment.
client, err := jev.NewWithKey(cfg.TypeSafeKey, jev.WithTimeout(5*time.Second))
func (*Client) Batch ¶
Batch starts a batch of questions about state. State may be a string or any value that marshals to JSON; named fields let questions refer to parts of it.
func (*Client) Models ¶
Models lists the models the account may use, through GET /v1/models.
It needs a provider that implements ModelLister, which the default HTTP provider does; otherwise it returns ErrNoModelList. Middleware does not hide it: the underlying provider is asked directly, so cached or instrumented clients still list models.
type Content ¶
type Content any
Content is anything that may be sent as instructions, as a choice option's description, as a score level or as a noul criterion: a plain string, or any value that marshals to a JSON object or array.
Structure is not decoration. A question with several parts reads more clearly as an object with named keys, and a taxonomy or a schema is already JSON, so it can be passed through instead of flattened into a sentence. A nil Content is sent as JSON null, which the API reads as "no description".
type FieldError ¶
type FieldError struct {
// Type is the kind of violation, such as "missing" or "too_short".
Type string
// Loc is the path to the offending field, such as ["body", "model"].
Loc []string
// Msg is the human-readable explanation.
Msg string
}
FieldError is one entry of a validation failure, as the API reports it on a 422 response.
func (FieldError) String ¶
func (f FieldError) String() string
String renders the entry as "path: message", dropping the leading "body" segment, as the official SDKs do.
type Handle ¶
type Handle[A any] struct { // contains filtered or unexported fields }
Handle is a typed slot for one answer in a Batch.
func Add ¶
Add appends q to b and returns a handle for its answer. An invalid question does not panic: its error is returned by Batch.Run and by Handle.Get.
type MemoryCache ¶
type MemoryCache struct {
// contains filtered or unexported fields
}
MemoryCache is an in-process LRU Cache.
func NewMemoryCache ¶
func NewMemoryCache(size int) *MemoryCache
NewMemoryCache returns an LRU cache holding up to size responses. A size below 1 defaults to 1024.
type Meta ¶
type Meta struct {
Model string
Usage Usage
// RequestID identifies the request in TypeSafe's logs. Quote it in a bug
// report. It is empty when the provider reports none.
RequestID string
Latency time.Duration
}
Meta describes the request a Batch made.
When the response came from a cache, Model, Usage and RequestID are those of the original request; only Latency describes this call.
type Middleware ¶
Middleware wraps a Provider, for caching, logging, metrics or rate limiting.
func CacheMiddleware ¶
func CacheMiddleware(cache Cache) Middleware
CacheMiddleware serves identical requests (same model, state and questions) from cache. Only successful responses are stored. Cached responses report the usage of the original request.
type ModelCard ¶
type ModelCard struct {
Name string `json:"name"`
Description string `json:"description"`
ReleaseDate time.Time `json:"release_date"`
}
ModelCard describes one model the account may use.
type ModelLister ¶
ModelLister is the optional half of Provider, implemented by providers that can also list models. Client.Models needs it. Middleware that wraps a provider hides it unless the wrapper implements it too.
type NoulAnswer ¶
type NoulAnswer struct {
P float64
}
NoulAnswer is the probability that the answer is yes. A noul carries no separate confidence: the probability itself is the signal.
func (NoulAnswer) Sure ¶
func (a NoulAnswer) Sure(threshold float64) (yes, ok bool)
Sure reports the answer and whether it clears threshold in either direction: (true, true) when P >= threshold, (false, true) when 1-P >= threshold, and (false, false) otherwise. Use a threshold above 0.5.
type NoulCriteria ¶
type NoulCriteria struct {
// True is what a value near 1 means.
True Content
// False is what a value near 0 means.
False Content
}
NoulCriteria describes the two ends of a Noul. Either side may be left nil, and either may be structured Content when the boundary is subtle.
type Option ¶
type Option func(*config)
Option configures a Client.
func WithAPIKey ¶
WithAPIKey sets the API key. Once it is given, TYPESAFE_API_KEY is not read at all: passing an empty key returns ErrNoAPIKey rather than quietly falling back to whatever the environment happens to hold. This is what the official SDKs do, and it keeps a client from reaching the API with a key its caller never chose.
func WithBaseURL ¶
WithBaseURL points the HTTP provider at another host, for proxies and tests. It overrides TYPESAFE_BASE_URL.
func WithCache ¶
WithCache caches responses for identical requests. See NewMemoryCache.
func WithHTTPClient ¶
WithHTTPClient supplies the http.Client used for requests. The client is never modified; per-attempt timeouts are applied through the request context.
func WithHeaders ¶
WithHeaders adds headers to every request. Authentication, content type and the SDK headers cannot be overridden.
func WithLogger ¶
WithLogger logs one line per request at info level, and retries as they happen. Nothing is logged without it.
func WithMiddleware ¶
func WithMiddleware(m ...Middleware) Option
WithMiddleware wraps the provider. The first middleware given is outermost.
func WithModel ¶
WithModel sets the default model, overriding TYPESAFE_DEFAULT_MODEL. Batch.UseModel overrides it per batch.
func WithProvider ¶
WithProvider replaces the HTTP provider, for fakes or alternative backends. No API key is required when a provider is supplied.
func WithRetries ¶
WithRetries sets how many times a retryable failure is retried, leaving the rest of the policy alone. Zero disables retries.
func WithRetry ¶
WithRetry replaces the retry policy. See DefaultRetry.
func WithTimeout ¶
WithTimeout bounds each HTTP attempt. Zero disables the per-attempt bound; the caller's context still applies.
type Provider ¶
Provider evaluates requests. The default provider talks to the TypeSafe HTTP API; tests use github.com/anilsenay/jev/jevtest. Implementations must be safe for concurrent use.
type ProviderFunc ¶
ProviderFunc adapts a function to Provider.
type Question ¶
type Question[A any] interface { // contains filtered or unexported methods }
Question is a typed question whose answer decodes to A. Build one with Noul, Choice, OneOf or Score. Questions are immutable values and are meant to be declared once, typically as package-level variables.
The interface is sealed: only this package can implement it, because the API accepts only these kinds.
func Choice ¶
func Choice[T ~string](instructions Content, options ...ChoiceOption[T]) Question[ChoiceAnswer[T]]
Choice picks exactly one of options and returns the distribution over them. Include a catch-all option when none of the others may fit: the model cannot answer outside the set.
func Noul ¶
func Noul(instructions Content, criteria ...NoulCriteria) Question[NoulAnswer]
Noul asks whether a condition holds and answers with the probability that it does. Pass a NoulCriteria to describe the two ends; filling them in usually sharpens the boundary. The API needs at least one of instructions and criteria.
Use one Noul per label when several labels can apply at once. A value near 0.5 means the model finds yes and no about equally likely; it does not mean "medium". Use a Score to place something on a spectrum.
func OneOf ¶
func OneOf[T ~string](instructions Content, values ...T) Question[ChoiceAnswer[T]]
OneOf is Choice without descriptions, for options whose names speak for themselves.
func RawQuestion ¶
RawQuestion sends a hand-built Spec exactly as given and hands the answer back undecoded. Nothing about it is validated or narrowed.
It exists so the API can move ahead of this package: a question kind or field that lands before a release does is still reachable, through Spec.Extra for new fields and a new Kind for a new type.
q := jev.RawQuestion(jev.Spec{
Type: jev.KindChoice,
Instructions: "Which team should handle this?",
Criteria: map[string]jev.Content{"billing": nil, "shipping": nil},
Extra: map[string]any{"beam_width": 4},
})
h := jev.Add(b, q) // *Handle[RawAnswer]
raw, err := h.Get() // read raw.Choice, raw.Probabilities yourself
Reach for a typed constructor whenever one fits; this gives up the type safety that is the point of the package.
func Score ¶
func Score(instructions Content, levels ...Content) Question[ScoreAnswer]
Score rates the state against ordered levels, lowest first. Each level should describe a concrete situation, as a string or as structured Content. The answer is a probability-weighted position, so it can land between levels.
type RawAnswer ¶
type RawAnswer struct {
Type Kind `json:"type"`
Noul *float64 `json:"noul,omitempty"`
Choice string `json:"choice,omitempty"`
Score *float64 `json:"score,omitempty"`
// Legend maps each score level index to the description that was sent. Its
// values are raw JSON because a level may be structured [Content].
Legend map[string]json.RawMessage `json:"legend,omitempty"`
Probabilities map[string]float64 `json:"probabilities,omitempty"`
Confidence *float64 `json:"confidence,omitempty"`
}
RawAnswer is an answer as it arrives over the wire, before it is narrowed to the type of its question.
type Request ¶
type Request struct {
// Model is required by the API. [Batch.Run] fills it from the client.
Model string `json:"model"`
State json.RawMessage `json:"state"`
Questions map[string]Spec `json:"questions"`
}
Request is one evaluation: a state and a battery of questions keyed by id.
type Response ¶
type Response struct {
Model string `json:"model"`
Answers map[string]RawAnswer `json:"answers"`
Usage Usage `json:"usage"`
// RequestID identifies the request in TypeSafe's logs. It is read from the
// response headers rather than the body, so it is not part of the JSON.
RequestID string `json:"-"`
}
Response is the result of one evaluation.
type Retry ¶
type Retry struct {
// MaxRetries is how many attempts follow the first. Zero disables retries.
MaxRetries int
// BackoffInitial is the first delay, doubled on each attempt up to
// BackoffMax. Zero disables backoff.
BackoffInitial time.Duration
// BackoffMax caps a single backoff delay.
BackoffMax time.Duration
// BackoffJitter is the fraction of each delay randomly subtracted, from 0
// to 1. It stops parallel callers from retrying in lockstep.
BackoffJitter float64
// Statuses lists the HTTP status codes to retry. Nil means the default:
// 408, 429 and every 5xx.
Statuses []int
// RespectRetryAfter honours the retry-after-ms and Retry-After headers.
RespectRetryAfter bool
// MaxRetryAfter is the longest server delay that is honoured. A longer one
// falls back to backoff.
MaxRetryAfter time.Duration
// RetryConnection retries failures that produced no HTTP response.
RetryConnection bool
// RetryTimeout retries attempts that ran out of time.
RetryTimeout bool
}
Retry configures how a failed request is retried. Its zero value is not useful; start from DefaultRetry and change what you need.
The defaults mirror the official TypeSafe SDKs: two retries, 500ms of initial backoff doubling to 5s with 25% jitter subtracted, honouring a server delay of up to a minute, retrying 408, 429 and every 5xx.
type ScoreAnswer ¶
type ScoreAnswer struct {
// Value runs from 0 (the first level) to len(Levels)-1.
Value float64
// Probs holds one probability per level, lowest level first.
Probs []float64
// Levels are the level descriptions from the question, in order.
Levels []Content
// Legend is the level descriptions the API echoed back, keyed by level
// index. It is nil when the answer carried none.
Legend map[int]Content
// Confidence is the API's confidence; see [ChoiceAnswer.Confidence].
Confidence float64
}
ScoreAnswer is a probability-weighted position across the levels of a Score.
func (ScoreAnswer) Nearest ¶
func (a ScoreAnswer) Nearest() (int, Content)
Nearest returns the index and description of the closest whole level. Use Text to render the description when the levels are structured.
func (ScoreAnswer) NearestIndex ¶
func (a ScoreAnswer) NearestIndex() int
NearestIndex returns the index of the closest whole level.
type Spec ¶
type Spec struct {
Type Kind `json:"type"`
// Instructions is optional, as it is in the official SDKs: a choice or a
// score whose criteria say everything can leave it nil, and it is then
// omitted from the request.
Instructions Content `json:"instructions,omitempty"`
// Criteria is *noulCriteria for noul questions, map[string]Content for
// choices (a nil description is sent as null) and []Content for scores.
Criteria any `json:"criteria,omitempty"`
// Extra carries question fields this package does not model, for API
// features that land before a release does. Its entries are marshalled
// alongside the three above, sorted by key; "type", "instructions" and
// "criteria" are reserved and ignored here. The typed constructors never
// set it; see [RawQuestion].
Extra map[string]any `json:"-"`
}
Spec is one question as it is sent over the wire. Most users never build a Spec directly; the typed constructors Noul, Choice, OneOf and Score produce them. Providers and fakes read them.
func (Spec) ChoiceOptions ¶
ChoiceOptions returns the option names of a choice, sorted. It returns nil for other kinds.
func (Spec) MarshalJSON ¶
MarshalJSON implements json.Marshaler, merging Spec.Extra into the question object.
func (Spec) ScoreLevels ¶
ScoreLevels reports how many levels a score has. It returns 0 for other kinds.