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:
| Value | Becomes |
|---|---|
A model id such as "jev-latest" or "jev-1.13.0" | TypeSafe(model_id) |
A DecisionClassifier, for example a TypeSafeClassifier | TypeSafe(classifier) |
Any object with name, model, headroom, and decide or adecide, for example TypeSafe or FakeBackend | Used as is |
None | No 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,
TypeSafecreatesTypeSafeClassifier(model=id, timeout=timeout)on the first request. It readsTYPESAFE_API_KEYandTYPESAFE_BASE_URL. An empty id, or atimeoutthat isn't positive, raisesPolicyError. - With a classifier,
modelis the classifier'smodel, andtimeoutis not applied. nameis"typesafe".- It has both
decideandadecide, and makes one attempt per request. max_request_bytescaps 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):
reason | Cause |
|---|---|
client_setup_failed | The classifier could not be created, most often because TYPESAFE_API_KEY is not set |
timeout | The request timed out |
malformed_response | The API response failed validation |
unauthorized | HTTP 401 or 403 |
rate_limited | HTTP 429 |
upstream_error | HTTP 5xx |
rejected | Any other HTTP 4xx |
connection_error | The API could not be reached |
unexpected_error | Any other exception |
malformed_answer | An 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.
Requestis one backend call.stateis theStateto judge.questionsis keyed by full question id,"<policy>.<question>".timeoutis the seconds left before the guard's deadline, orNone.Replyholds one answer per question id, and the tokens the call used, when the backend reports them. The guard turns each reply into aUsage.
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: ...
nameidentifies the backend in errors.modelis recorded on everyScoreResultandUsage.headroomreturns the bytes left in a request for this state and these questions. Negative means the text doesn't fit.Nonemeans no limit. A guard calls it with empty text when it is built. Less than 64 bytes of room raisesPolicyError.Guardcallsdecide. A backend with onlyadecideraisesPolicyErrorwhen you build aGuard; useAsyncGuard.AsyncGuardprefersadecide, and runsdecidein a worker thread.- Raise
BackendErrorfor your own failures. Any other exception becomesBackendError(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
| Name | Purpose |
|---|---|
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 |