gcoderw's picture
Rename package to deg (DEG_ env vars, deg.json); note upcoming modalities
16d760d verified
Raw History Blame Contribute Delete
31.7 kB
"""HTTP API contracts on top of :func:`deg.infer.fill` (SPEC.md §9).
Three request shapes are translated into one deg fill (one schema, one context read) and
the fill response is translated back:
* ``POST /v1/decisions``: the OpenAI Decisions API (public beta, 2026-10-06). Field names
and answer objects mirror ``openai-python`` 3.26.1 ``types/decision.py`` and
``types/decision_create_params.py`` and the guide at
https://developers.openai.com/api/docs/guides/decisions: ``model``, ``input`` (string or
user messages of ``input_text`` parts), ``questions`` (``predicate`` / ``choice`` /
``score``, each with an optional ``name``) -> ``{answers: [...], model, usage}`` with
answers in question order.
* ``POST /v1/systemone``: TypeSafe Jev / SystemOne (``state`` + a map of ``noul`` /
``choice`` / ``score`` questions). Shapes, limits and error bodies follow live
``api.typesafe.ai`` responses from ``jev-1.13.0`` (2026-10-09): answers keyed by question
id in request order, ``{"type": "noul", "noul": p}``, choice ``{type, choice, confidence,
probabilities{option: p}}``, score ``{type, score, confidence, legend{"i": level},
probabilities{"i": p}}``, usage ``{input_tokens, output_tokens}``; 400 ``{"detail":
{"error_type": "api_usage_error", "message": ...}}`` for semantic errors and 422
``{"detail": [{type, loc, msg, input}]}`` for missing fields.
* ``POST /v1/fill``: deg's own contract (:mod:`deg.request`), returned unchanged.
Mapping (the same in both decision APIs): ``predicate`` = ``noul`` -> ``boolean``
(decide.noul); ``choice`` -> ``enum`` / ``oneOf`` of ``const`` with descriptions
(decide.choice); ``score`` -> ``integer`` levels ``0..L-1`` (decide.score) with level labels
(and descriptions, ``label: description``) as ``x-levels``, so each becomes that level's
option line ``i: label`` (the jev training layout); the description keeps only the
instructions. One-option choices and one-level scores are
answered without the model (probability 1), as Jev does.
deg extensions, opt-in (stock clients never send them):
* ``extract``: a value copied from the input. ``value_type`` is ``string`` (default),
``number``, ``integer``, ``date``, ``date-time``, ``email``, ``uri`` or ``uuid``; optional
``pattern``, ``min_length``, ``max_length`` (string types). Always nullable: the answer is
``{type: "extract", value, span: [start, end) | null, confidence}`` plus ``null_reason``
when ``value`` is null. ``span`` indexes code points of the rendered input.
* ``generate``: free text (the only new tokens), with optional ``max_tokens`` (that
question's own budget, ``x-max-tokens``; questions in one request may differ),
``min_length``, ``max_length``, ``pattern``; greedy unless ``options.temperature`` > 0
(then ``options.seed`` is required). Generated after every other question and conditioned
on their answers. Answer: ``{type: "generate", text, output_tokens}``.
* a request-level ``options`` object: ``temperature``, ``seed``, ``date_order``,
``default_offset``, ``array_decode``, ``null_margin`` (as in ``/v1/fill``).
"""
from __future__ import annotations
import json
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
from deg.request import MODEL_ALIASES, FillOptions, parse_options
from deg.response import MODEL_ID
from deg.schema import SchemaError, compile_schema
MAX_CHOICES = 255 # Jev: "Too many choices. Must have at most 255 choices."
MAX_LEVELS = 10 # Jev: "Too many score levels. Must have at most 10 levels."
OPENAI_COMPAT_MODELS = frozenset({"gpt-6-luna"}) # served by deg when a stock client sends it
JEV_COMPAT_MODELS = frozenset({"jev-latest", "jev-preview", "clef-flash"})
EXTRACT_TYPES = {
"string": ("string", None),
"number": ("number", None),
"integer": ("integer", None),
"date": ("string", "date"),
"date-time": ("string", "date-time"),
"email": ("string", "email"),
"uri": ("string", "uri"),
"uuid": ("string", "uuid"),
}
REQUEST_OPTION_KEYS = frozenset({"temperature", "seed", "date_order", "default_offset",
"array_decode", "null_margin"})
DECIDE_TYPES = ("predicate", "choice", "score")
EXTENSION_TYPES = ("extract", "generate")
FillFn = Callable[[Any, dict[str, Any], FillOptions], dict[str, Any]]
class ApiError(Exception):
"""A client (4xx) or server (5xx) error with the API's own body shape."""
def __init__(self, status: int, body: dict[str, Any], message: str) -> None:
super().__init__(message)
self.status = status
self.body = body
# ------------------------------------------------------------------------- questions
@dataclass(frozen=True)
class Question:
"""One question in either decision API, in deg terms."""
key: str # property name in the fill schema (unique)
kind: str # noul | choice | score | extract | generate
instructions: str
name: str | None = None # OpenAI echo name (None when unnamed)
options: tuple[Any, ...] = () # choice values (typed)
descriptions: tuple[str | None, ...] = () # choice descriptions / score level texts
labels: tuple[str, ...] = () # score level labels
true_criterion: str | None = None
false_criterion: str | None = None
extra: dict[str, Any] = field(default_factory=dict) # extract/generate constraints
@property
def trivial(self) -> bool:
"""A one-option choice or one-level score: answered with probability 1."""
return (self.kind == "choice" and len(self.options) == 1) or (
self.kind == "score" and len(self.labels) == 1)
def field_schema(self) -> dict[str, Any]:
if self.kind == "noul":
desc = self.instructions
if self.true_criterion:
desc += f"\nYes means: {self.true_criterion}"
if self.false_criterion:
desc += f"\nNo means: {self.false_criterion}"
return {"type": "boolean", "description": desc}
if self.kind == "choice":
if any(self.descriptions):
return {"description": self.instructions, "oneOf": [
{"const": v, **({"description": d} if d else {})}
for v, d in zip(self.options, self.descriptions)]}
return {"description": self.instructions, "enum": list(self.options)}
if self.kind == "score": # level texts as option lines via x-levels (SPEC.md §2)
return {"type": "integer", "minimum": 0, "maximum": len(self.labels) - 1,
"description": self.instructions,
"x-levels": {str(i): lab + (f": {d}" if d else "")
for i, (lab, d) in enumerate(zip(self.labels,
self.descriptions))}}
if self.kind == "extract":
json_type, fmt = EXTRACT_TYPES[self.extra.get("value_type", "string")]
out: dict[str, Any] = {"type": [json_type, "null"], "description": self.instructions}
if fmt:
out["format"] = fmt
for src, dst in (("pattern", "pattern"), ("min_length", "minLength"),
("max_length", "maxLength")):
if src in self.extra:
out[dst] = self.extra[src]
return out
out = {"type": "string", "x-generate": True, "description": self.instructions}
for src, dst in (("pattern", "pattern"), ("min_length", "minLength"),
("max_length", "maxLength"), ("max_tokens", "x-max-tokens")):
if src in self.extra:
out[dst] = self.extra[src]
return out
@dataclass(frozen=True)
class Job:
"""One fill to run: ``schema`` is None when every question is trivial."""
context: Any
schema: dict[str, Any] | None
options: FillOptions
questions: tuple[Question, ...]
@property
def question_types(self) -> tuple[str, ...]:
return tuple(q.kind for q in self.questions)
def _is_int(value: Any) -> bool:
return isinstance(value, int) and not isinstance(value, bool)
def _constraint_errors(kind: str, raw: dict[str, Any], add: Callable[[str, str], None]
) -> dict[str, Any]:
"""Validate extract/generate keys; returns the constraint dict (``extra``)."""
extra: dict[str, Any] = {}
if kind == "extract":
vt = raw.get("value_type", "string")
if vt not in EXTRACT_TYPES:
add("value_type", "value_type must be one of " + ", ".join(EXTRACT_TYPES))
return extra
extra["value_type"] = vt
string_like = EXTRACT_TYPES[vt][0] == "string" and EXTRACT_TYPES[vt][1] is None
else:
string_like = True
if "max_tokens" in raw:
if not _is_int(raw["max_tokens"]) or raw["max_tokens"] < 1:
add("max_tokens", "max_tokens must be a positive integer")
else:
extra["max_tokens"] = raw["max_tokens"]
for key in ("min_length", "max_length"):
if key in raw:
if not string_like:
add(key, f"{key} applies only to string values")
elif not _is_int(raw[key]) or raw[key] < 0:
add(key, f"{key} must be a non-negative integer")
else:
extra[key] = raw[key]
if "pattern" in raw:
if not string_like:
add("pattern", "pattern applies only to string values")
elif not isinstance(raw["pattern"], str):
add("pattern", "pattern must be a string")
else:
extra["pattern"] = raw["pattern"]
return extra
def _request_options(raw: Any, add: Callable[[str, str], None]) -> FillOptions:
if raw is None:
return FillOptions()
if not isinstance(raw, dict):
add("options", "options must be an object")
return FillOptions()
errors: list[dict[str, Any]] = []
for key in raw:
if key not in REQUEST_OPTION_KEYS:
errors.append({"loc": ["options", key], "msg": "unknown option"})
opts = parse_options({k: v for k, v in raw.items() if k in REQUEST_OPTION_KEYS}, errors)
for e in errors:
add(".".join(map(str, e["loc"][-2:] if e["loc"][0] == "body" else e["loc"])), e["msg"])
return opts
def build_job(context: Any, questions: list[Question], options: FillOptions
) -> tuple[Job, list[tuple[str, str]]]:
"""The fill schema for these questions; schema compile errors come back as (loc, msg)."""
live = [q for q in questions if not q.trivial]
if not live:
return Job(context, None, options, tuple(questions)), []
schema = {"type": "object",
"properties": {q.key: q.field_schema() for q in live},
"required": [q.key for q in live],
"additionalProperties": False}
try:
compile_schema(schema)
except SchemaError as exc:
return Job(context, None, options, tuple(questions)), [
(".".join(map(str, e["loc"])), e["msg"]) for e in exc.errors]
return Job(context, schema, options, tuple(questions)), []
def _pointer(key: str) -> str:
return "/" + key.replace("~", "~0").replace("/", "~1")
def _expected(probabilities: list[float]) -> float:
return sum(i * p for i, p in enumerate(probabilities))
@dataclass(frozen=True)
class Outcome:
"""A question's deg outcome, API-neutral."""
kind: str
value: Any = None
probability: float | None = None # noul p(true)
probabilities: tuple[float, ...] = ()
confidence: float | None = None
span: list[int] | None = None
null_reason: str | None = None
output_tokens: int = 0
def outcomes(job: Job, response: dict[str, Any] | None) -> list[Outcome]:
"""Per-question outcomes from a fill response (``None`` when nothing was filled)."""
out = []
for q in job.questions:
if q.trivial:
value = q.options[0] if q.kind == "choice" else 0
out.append(Outcome(q.kind, value, probabilities=(1.0,), confidence=1.0))
continue
assert response is not None
meta = response["meta"][_pointer(q.key)]
value = response["value"].get(q.key)
if q.kind == "noul":
out.append(Outcome("noul", value, probability=float(meta["probability"])))
elif q.kind in ("choice", "score"):
out.append(Outcome(q.kind, value, probabilities=tuple(meta["probabilities"]),
confidence=float(meta["confidence"])))
elif q.kind == "extract":
out.append(Outcome("extract", value, confidence=float(meta["confidence"]),
span=meta.get("span"), null_reason=meta.get("null_reason")))
else:
out.append(Outcome("generate", value,
output_tokens=int(meta.get("generated_tokens", 0))))
return out
def _usage_tokens(response: dict[str, Any] | None) -> tuple[int, int]:
if response is None:
return 0, 0
usage = response.get("usage") or {}
return int(usage.get("prompt_tokens", 0)), int(usage.get("generated_tokens", 0))
def _extension_answer(o: Outcome) -> dict[str, Any]:
if o.kind == "extract":
ans = {"type": "extract", "value": o.value, "span": o.span, "confidence": o.confidence}
if o.value is None:
ans["null_reason"] = o.null_reason or "not_found"
return ans
return {"type": "generate", "text": o.value, "output_tokens": o.output_tokens}
def _text(value: Any) -> str:
return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
# ------------------------------------------------------------------- OpenAI Decisions
def openai_error(status: int, message: str, param: str | None = None,
kind: str = "invalid_request_error", code: str | None = None) -> ApiError:
return ApiError(status, {"error": {"message": message, "type": kind, "param": param,
"code": code}}, message)
def _openai_input(raw: Any, add: Callable[[str, str], None]) -> str:
"""Text the model reads: the string, or the user messages' text parts joined by blank
lines. Images are refused (deg is text-only)."""
if isinstance(raw, str):
if not raw.strip():
add("input", "input must be a non-empty string")
return raw
if not isinstance(raw, list) or not raw:
add("input", "input must be a string or a non-empty array of user messages")
return ""
texts: list[str] = []
for i, msg in enumerate(raw):
loc = f"input[{i}]"
if not isinstance(msg, dict):
add(loc, "each input item must be a message object")
continue
for key in msg:
if key not in ("role", "content", "type"):
add(f"{loc}.{key}", f"Unrecognized message field: {key}")
if msg.get("type", "message") != "message":
add(f"{loc}.type", "only message items are supported")
if msg.get("role") != "user":
add(f"{loc}.role", "only user messages are supported")
content = msg.get("content")
if isinstance(content, str):
texts.append(content)
continue
if not isinstance(content, list) or not content:
add(f"{loc}.content", "content must be a string or a non-empty array of parts")
continue
for j, part in enumerate(content):
ploc = f"{loc}.content[{j}]"
ptype = part.get("type") if isinstance(part, dict) else None
if ptype == "input_text" and isinstance(part.get("text"), str):
texts.append(part["text"])
elif ptype == "input_image":
add(ploc, "input_image is not supported: this deg model reads text only")
else:
add(ploc, "each part must be an input_text part with a string text")
text = "\n\n".join(texts)
if not text.strip():
add("input", "input has no text")
return text
def _openai_question(i: int, raw: Any, add: Callable[[str, str], None]) -> Question | None:
loc = f"questions[{i}]"
if not isinstance(raw, dict):
add(loc, "each question must be an object")
return None
kind = raw.get("type")
allowed = {"predicate": {"type", "name", "instructions"},
"choice": {"type", "name", "instructions", "choices"},
"score": {"type", "name", "instructions", "levels"},
"extract": {"type", "name", "instructions", "value_type", "pattern",
"min_length", "max_length"},
"generate": {"type", "name", "instructions", "max_tokens", "pattern",
"min_length", "max_length"}}.get(kind)
if allowed is None:
add(f"{loc}.type", "type must be one of predicate, choice, score (deg extensions: "
"extract, generate)")
return None
n = 0
def qadd(key: str, msg: str) -> None:
nonlocal n
n += 1
add(f"{loc}.{key}", msg)
for key in raw:
if key not in allowed:
qadd(key, f"Unrecognized question field for type {kind}: {key}")
name = raw.get("name")
if name is not None and (not isinstance(name, str) or not name):
qadd("name", "name must be a non-empty string")
name = None
instructions = raw.get("instructions")
if not isinstance(instructions, str) or not instructions.strip():
qadd("instructions", "instructions must be a non-empty string")
key = name if name is not None else f"question_{i}"
if kind == "predicate":
q = Question(key, "noul", instructions or "", name)
elif kind == "choice":
choices = raw.get("choices")
if not isinstance(choices, list) or not choices:
qadd("choices", "choices must be a non-empty array")
return None
if len(choices) > MAX_CHOICES:
qadd("choices", f"Too many choices. Must have at most {MAX_CHOICES} choices.")
values, descs, seen = [], [], set()
for j, ch in enumerate(choices):
cloc = f"choices[{j}]"
if not isinstance(ch, dict) or set(ch) - {"value", "description"}:
qadd(cloc, "each choice must be {value, description?}")
continue
value, desc = ch.get("value"), ch.get("description")
if not isinstance(value, (str, bool)) or value == "":
qadd(f"{cloc}.value", "value must be a non-empty string or a boolean")
continue
if desc is not None and not isinstance(desc, str):
qadd(f"{cloc}.description", "description must be a string")
typed = (type(value).__name__, value)
if typed in seen:
qadd(f"{cloc}.value", f"duplicate choice value {value!r}")
seen.add(typed)
values.append(value)
descs.append(desc or None)
q = Question(key, "choice", instructions or "", name, tuple(values), tuple(descs))
elif kind == "score":
levels = raw.get("levels")
if not isinstance(levels, list) or not levels:
qadd("levels", "levels must be a non-empty array")
return None
if len(levels) > MAX_LEVELS:
qadd("levels", f"Too many score levels. Must have at most {MAX_LEVELS} levels.")
labels, descs = [], []
for j, lv in enumerate(levels):
lloc = f"levels[{j}]"
if (not isinstance(lv, dict) or set(lv) - {"label", "description"}
or not isinstance(lv.get("label"), str) or not lv["label"].strip()
or not isinstance(lv.get("description", ""), str)):
qadd(lloc, "each level must be {label: non-empty string, description?: string}")
continue
labels.append(lv["label"])
descs.append(lv.get("description") or None)
q = Question(key, "score", instructions or "", name, descriptions=tuple(descs),
labels=tuple(labels))
else:
extra = _constraint_errors(kind, raw, qadd)
q = Question(key, kind, instructions or "", name, extra=extra)
return None if n else q
def parse_decisions(payload: Any, accepted_models: frozenset[str] = MODEL_ALIASES
| OPENAI_COMPAT_MODELS) -> Job:
"""Validate an OpenAI Decisions request. Raises :class:`ApiError` (400)."""
if not isinstance(payload, dict):
raise openai_error(400, "The request body must be a JSON object.")
errors: list[tuple[str, str]] = []
def add(param: str, msg: str) -> None:
errors.append((param, msg))
for key in payload:
if key not in ("model", "input", "questions", "safety_identifier", "options"):
add(key, f"Unrecognized request argument supplied: {key}")
model = payload.get("model")
if not isinstance(model, str):
add("model", "Missing required parameter: 'model'.")
elif model not in accepted_models:
add("model", f"The model `{model}` does not exist or you do not have access to it.")
if "input" not in payload:
add("input", "Missing required parameter: 'input'.")
text = ""
else:
text = _openai_input(payload["input"], add)
sid = payload.get("safety_identifier")
if sid is not None and not isinstance(sid, str):
add("safety_identifier", "safety_identifier must be a string")
options = _request_options(payload.get("options"), add)
raw_questions = payload.get("questions")
questions: list[Question] = []
if not isinstance(raw_questions, list) or not raw_questions:
add("questions", "questions must be a non-empty array")
else:
for i, raw in enumerate(raw_questions):
q = _openai_question(i, raw, add)
if q is not None:
questions.append(q)
names = [q.get("name") for q in raw_questions if isinstance(q, dict) and q.get("name")]
dup = sorted({n for n in names if isinstance(n, str) and names.count(n) > 1})
if dup:
add("questions", f"question names must be unique: {dup}")
keys = [q.key for q in questions]
if len(set(keys)) != len(keys) and not dup:
add("questions", "a question name collides with an unnamed question's key "
"(question_<index>)")
job = None
if not errors:
job, schema_errors = build_job(text, questions, options)
for loc, msg in schema_errors:
add("questions", f"{loc}: {msg}")
if errors:
param, _ = errors[0]
raise openai_error(400, "; ".join(f"{p}: {m}" for p, m in errors), param)
assert job is not None
return job
def decisions_response(job: Job, response: dict[str, Any] | None,
model: str = MODEL_ID) -> dict[str, Any]:
"""The ``Decision`` object (openai-python ``types/decision.py``)."""
answers = []
for q, o in zip(job.questions, outcomes(job, response), strict=True):
if o.kind == "noul":
answers.append({"type": "predicate", "name": q.name, "probability": o.probability})
elif o.kind == "choice":
answers.append({"type": "choice", "name": q.name, "choice": o.value,
"probabilities": [{"value": v, "probability": p}
for v, p in zip(q.options, o.probabilities)],
"confidence": o.confidence})
elif o.kind == "score":
probs = list(o.probabilities)
answers.append({"type": "score", "name": q.name, "score": _expected(probs),
"probabilities": [{"value": i, "label": lab, "probability": p}
for i, (lab, p) in enumerate(zip(q.labels, probs))],
"confidence": o.confidence})
else:
ext = _extension_answer(o)
answers.append({"type": ext.pop("type"), "name": q.name, **ext})
inp, out = _usage_tokens(response)
return {"answers": answers, "model": model,
"usage": {"input_tokens": inp,
"input_tokens_details": {"cache_write_tokens": 0, "cached_tokens": 0},
"output_tokens": out,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": inp + out}}
# --------------------------------------------------------------------- Jev SystemOne
def jev_error(message: str, status: int = 400, error_type: str = "api_usage_error",
*, plain: bool = False) -> ApiError:
"""Jev's 400 body; ``plain`` = a bare string ``detail`` (live Jev's limit errors)."""
detail: Any = message if plain else {"error_type": error_type, "message": message}
return ApiError(status, {"detail": detail}, message)
def jev_validation_error(items: list[dict[str, Any]]) -> ApiError:
return ApiError(422, {"detail": items}, "; ".join(i["msg"] for i in items))
def _jev_structured(value: Any) -> bool:
if isinstance(value, str):
return bool(value.strip())
if isinstance(value, (dict, list)) and value:
try:
json.dumps(value, allow_nan=False)
except (TypeError, ValueError):
return False
return True
return False
def _jev_question(qid: str, raw: Any) -> Question:
"""One SystemOne question; raises :class:`ApiError` (Jev bodies)."""
loc = ["body", "questions", qid]
if not isinstance(raw, dict):
raise jev_validation_error([{"type": "model_type", "loc": loc,
"msg": "Input should be a valid dictionary", "input": raw}])
kind = raw.get("type")
if kind not in ("noul", "choice", "score", "extract", "generate"):
raise jev_error("Invalid request.")
if "instructions" not in raw:
raise jev_validation_error([{"type": "missing", "loc": [*loc, "instructions"],
"msg": "Field required", "input": raw}])
instructions = raw["instructions"]
if not _jev_structured(instructions):
raise jev_error("Invalid request.")
text = _text(instructions)
criteria = raw.get("criteria")
if kind == "noul":
true_c = false_c = None
if criteria is not None:
if not isinstance(criteria, dict) or set(criteria) - {"true", "false"} or not all(
v is None or isinstance(v, str) for v in criteria.values()):
raise jev_error("Invalid request.")
true_c, false_c = criteria.get("true"), criteria.get("false")
return Question(qid, "noul", text, true_criterion=true_c, false_criterion=false_c)
if kind == "choice":
if not isinstance(criteria, dict) or not criteria:
raise jev_error("Invalid request.")
if len(criteria) > MAX_CHOICES:
raise jev_error(f"Too many choices. Must have at most {MAX_CHOICES} choices.",
plain=True)
if any(not k.strip() or (d is not None and not isinstance(d, str))
for k, d in criteria.items()):
raise jev_error("Invalid request.")
return Question(qid, "choice", text, options=tuple(criteria),
descriptions=tuple(criteria.values()))
if kind == "score":
if not isinstance(criteria, list) or not criteria:
raise jev_error("Invalid request.")
if len(criteria) > MAX_LEVELS:
raise jev_error(f"Too many score levels. Must have at most {MAX_LEVELS} levels.",
plain=True)
if any(not isinstance(c, str) or not c.strip() for c in criteria):
raise jev_error("Invalid request.")
return Question(qid, "score", text, descriptions=(None,) * len(criteria),
labels=tuple(criteria))
errors: list[str] = []
extra = _constraint_errors(kind, raw, lambda k, m: errors.append(f"{qid}.{k}: {m}"))
if errors:
raise jev_error("; ".join(errors))
return Question(qid, kind, text, extra=extra)
def parse_systemone(payload: Any, accepted_models: frozenset[str] = MODEL_ALIASES
| JEV_COMPAT_MODELS) -> Job:
"""Validate a SystemOne request (live Jev semantics). Raises :class:`ApiError`."""
if not isinstance(payload, dict):
raise jev_validation_error([{"type": "model_attributes_type", "loc": ["body"],
"msg": "Input should be a valid dictionary or object to "
"extract fields from", "input": payload}])
missing = [{"type": "missing", "loc": ["body", k], "msg": "Field required", "input": payload}
for k in ("model", "state", "questions") if k not in payload]
if missing:
raise jev_validation_error(missing)
questions = payload["questions"]
if not isinstance(questions, dict):
raise jev_validation_error([{"type": "dict_type", "loc": ["body", "questions"],
"msg": "Input should be a valid dictionary",
"input": questions}])
if not questions:
raise jev_validation_error([{
"type": "too_short", "loc": ["body", "questions"],
"msg": "Dictionary should have at least 1 item after validation, not 0",
"input": {}, "ctx": {"field_type": "Dictionary", "min_length": 1,
"actual_length": 0}}])
if set(payload) - {"model", "state", "questions", "options"}:
raise jev_error("Invalid request.")
model = payload["model"]
if not isinstance(model, str) or model not in accepted_models:
raise jev_error(f"Unknown model: {model}")
if not _jev_structured(payload["state"]):
raise jev_error("Invalid request.")
parsed = [_jev_question(qid, raw) for qid, raw in questions.items()]
if any(not qid for qid in questions):
raise jev_error("Invalid request.")
errors: list[str] = []
options = _request_options(payload.get("options"), lambda k, m: errors.append(f"{k}: {m}"))
if errors:
raise jev_error("; ".join(errors))
job, schema_errors = build_job(payload["state"], parsed, options)
if schema_errors:
raise jev_error("; ".join(f"{loc}: {msg}" for loc, msg in schema_errors))
return job
def systemone_response(job: Job, response: dict[str, Any] | None,
model: str = MODEL_ID) -> dict[str, Any]:
answers: dict[str, Any] = {}
for q, o in zip(job.questions, outcomes(job, response), strict=True):
if o.kind == "noul":
answers[q.key] = {"type": "noul", "noul": o.probability}
elif o.kind == "choice":
answers[q.key] = {"type": "choice", "choice": o.value, "confidence": o.confidence,
"probabilities": dict(zip(q.options, o.probabilities))}
elif o.kind == "score":
probs = list(o.probabilities)
answers[q.key] = {"type": "score", "score": _expected(probs),
"confidence": o.confidence,
"legend": {str(i): lab for i, lab in enumerate(q.labels)},
"probabilities": {str(i): p for i, p in enumerate(probs)}}
else:
answers[q.key] = _extension_answer(o)
inp, out = _usage_tokens(response)
return {"model": model, "answers": answers,
"usage": {"input_tokens": inp, "output_tokens": out}}