Skip to main content

jes.backend

from jes.backend import TypeSafe, ModelSpec, Request, Reply, resolve_model

A backend answers the questions a check asks about a text. jes ships one, TypeSafe, for decision models on the TypeSafe API, such as Jev (the default) or Laya. The guide is The judge.

ModelSpec​

ModelSpec = str | DecisionClassifier | SyncBackend | AsyncBackend

What model= accepts on Guard, AsyncGuard, judge(), every judgment factory, and the judgment recipes:

ValueBecomes
A model id such as "jev-latest" or "jev-1.13.0"TypeSafe(model_id)
A DecisionClassifier, for example a TypeSafeClassifierTypeSafe(classifier)
Any object with name, model, headroom, and decide or adecide, for example TypeSafe or FakeBackendUsed as is
NoneNo model. A policy falls back to the guard's model.

Anything else raises PolicyError. So does a judgment that ends up with no model.

resolve_model​

resolve_model(spec: ModelSpec | None) -> SyncBackend | AsyncBackend | None

Turns a ModelSpec into a backend, following the table above.

TypeSafe​

TypeSafe(
model: str | DecisionClassifier = "jev-latest",
*,
timeout: float = 30.0,
max_request_bytes: int = 1_048_576,
)

The backend that sends a check's questions to a TypeSafe decision model through LangChain's TypeSafeClassifier. You rarely build one yourself, because model= does it for you. Build one to change timeout or max_request_bytes.

  • With a model id, TypeSafe creates TypeSafeClassifier(model=id, timeout=timeout) on the first request. It reads TYPESAFE_API_KEY and TYPESAFE_BASE_URL. An empty id, or a timeout that isn't positive, raises PolicyError.
  • With a classifier, model is the classifier's model, and timeout is not applied.
  • name is "typesafe".
  • It has both decide and adecide, and makes one attempt per request.
  • max_request_bytes caps the UTF-8 size of the rendered state plus an estimate for the questions. It is not the model's token window.

Errors​

A failed request raises BackendError("typesafe", reason):

reasonCause
client_setup_failedThe classifier could not be created, most often because TYPESAFE_API_KEY is not set
timeoutThe request timed out
malformed_responseThe API response failed validation
unauthorizedHTTP 401 or 403
rate_limitedHTTP 429
upstream_errorHTTP 5xx
rejectedAny other HTTP 4xx
connection_errorThe API could not be reached
unexpected_errorAny other exception
malformed_answerAn answer is missing, of the wrong type, or its probabilities don't sum to one

status_code is set for HTTP errors. How a check treats the error depends on the guard's on_backend_error. See Errors.

Request and Reply​

Request(state: State, questions: Mapping[str, Question], timeout: float | None = None)

Reply(answers: Mapping[str, Answer], input_tokens: int | None = None,
output_tokens: int | None = None)

Both are frozen dataclasses.

  • Request is one backend call. state is the State to judge. questions is keyed by full question id, "<policy>.<question>". timeout is the seconds left before the guard's deadline, or None.
  • Reply holds one answer per question id, and the tokens the call used, when the backend reports them. The guard turns each reply into a Usage.

A reply that isn't a Reply, or that lacks an answer for a question it was asked, fails with reason malformed_reply.

Backend protocols​

To bring your own judge, implement SyncBackend, AsyncBackend, or both, and pass the object as model=. Custom backends always need explicit thresholds.

class Backend(Protocol):
@property
def name(self) -> str: ...
@property
def model(self) -> str: ...
def headroom(self, state: State, questions: Mapping[str, Question]) -> int | None: ...

class SyncBackend(Backend, Protocol):
def decide(self, request: Request) -> Reply: ...

class AsyncBackend(Backend, Protocol):
async def adecide(self, request: Request) -> Reply: ...
  • name identifies the backend in errors. model is recorded on every ScoreResult and Usage.
  • headroom returns the bytes left in a request for this state and these questions. Negative means the text doesn't fit. None means no limit. A guard calls it with empty text when it is built. Less than 64 bytes of room raises PolicyError.
  • Guard calls decide. A backend with only adecide raises PolicyError when you build a Guard; use AsyncGuard. AsyncGuard prefers adecide, and runs decide in a worker thread.
  • Raise BackendError for your own failures. Any other exception becomes BackendError(reason="unexpected_error").

All four protocols are runtime_checkable.

DecisionClassifier​

class DecisionClassifier(Protocol):
model: str
def invoke(self, input: Any, config: Any = None, **kwargs: Any) -> Any: ...
async def ainvoke(self, input: Any, config: Any = None, **kwargs: Any) -> Any: ...

LangChain's TypeSafeClassifier, or anything with the same calls. The input is {"state": str, "questions": {id: Noul | Choice | Score}}. The response must have an answers mapping with one TypeSafe answer per id, and may have a usage with input_tokens and output_tokens.

Helpers​

NamePurpose
render_state(state)The string sent as state: the text alone, or compact JSON with text, prompt, question, sources, history, and tool when there is context
typesafe_questions(questions)Maps jes questions to TypeSafe Noul, Choice, and Score