areneau commited on
Commit
a20e258
·
verified ·
1 Parent(s): bace2d8

Add systemone: Jev/SystemOne-compatible request and response API

Browse files
Files changed (2) hide show
  1. README.md +28 -1
  2. joint_schema_model.py +62 -1
README.md CHANGED
@@ -43,7 +43,7 @@ smaller, faster variant.
43
  |---|---|
44
  | `model-*.safetensors`, `model.safetensors.index.json`, `config.json`, `generation_config.json` | Backbone, including the vision encoder |
45
  | `joint_head.safetensors`, `joint_head_config.json` | Joint schema head |
46
- | `joint_schema_model.py` | Record encoding, batching, the model, and `load_release_model` |
47
  | `tokenizer.json`, `tokenizer_config.json`, `chat_template.jinja`, `processor_config.json` | Tokenizer and image/video processor |
48
  | `LICENSE` | Apache-2.0 license |
49
 
@@ -86,6 +86,33 @@ for question, question_logits in zip(encoded.questions, logits):
86
  print(question.question_id, dict(zip(question.option_ids, probabilities)))
87
  ```
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  ### Images and video
90
 
91
  Add `images` (PIL images) or `videos` (frame arrays) to the record and pass the processor to
 
43
  |---|---|
44
  | `model-*.safetensors`, `model.safetensors.index.json`, `config.json`, `generation_config.json` | Backbone, including the vision encoder |
45
  | `joint_head.safetensors`, `joint_head_config.json` | Joint schema head |
46
+ | `joint_schema_model.py` | Record encoding, batching, the model, `load_release_model`, and `systemone` |
47
  | `tokenizer.json`, `tokenizer_config.json`, `chat_template.jinja`, `processor_config.json` | Tokenizer and image/video processor |
48
  | `LICENSE` | Apache-2.0 license |
49
 
 
86
  print(question.question_id, dict(zip(question.option_ids, probabilities)))
87
  ```
88
 
89
+ ### Jev / SystemOne API
90
+
91
+ `systemone` takes a Jev/SystemOne `POST /v1/systemone` request body and returns the same response
92
+ body: `model`, `answers` keyed by question ID, and `usage`. A `choice` answer has `choice`,
93
+ `confidence`, and `probabilities`; a `score` answer has the expected `score`, `confidence`, `legend`,
94
+ and `probabilities`; a `noul` answer has the probability of true. `instructions` is optional, and
95
+ `images` and `videos` may be added to the request.
96
+
97
+ ```python
98
+ from joint_schema_model import systemone
99
+
100
+ response = systemone(model, processor, {
101
+ "model": "clef",
102
+ "state": "Our checkout started returning errors and orders are blocked.",
103
+ "questions": {
104
+ "department": {
105
+ "type": "choice",
106
+ "instructions": "Which team should handle the message?",
107
+ "criteria": {"billing": "Payments or invoices", "technical": "Bugs or outages"},
108
+ },
109
+ "urgency": {"type": "score", "criteria": ["Can wait", "This week", "Today"]},
110
+ "outage": {"type": "noul", "instructions": "Is a service down?"},
111
+ },
112
+ })
113
+ print(response["answers"])
114
+ ```
115
+
116
  ### Images and video
117
 
118
  Add `images` (PIL images) or `videos` (frame arrays) to the record and pass the processor to
joint_schema_model.py CHANGED
@@ -4,6 +4,8 @@ A record provides a ``state`` (any JSON value), optional ``images`` and ``videos
4
  and ``questions``. Each question has a ``type`` (``noul``, ``choice``, or ``score``),
5
  ``instructions``, and, for ``choice`` and ``score``, ``criteria`` describing the
6
  allowed options. The model returns one logit per allowed option for every question.
 
 
7
  """
8
 
9
  from __future__ import annotations
@@ -115,7 +117,10 @@ def encode_record(
115
  )
116
  )
117
  question_start = len(schema_ids)
118
- schema_ids.extend(_tokens(tokenizer, render(question["instructions"])))
 
 
 
119
  question_end = len(schema_ids)
120
  schema_ids.extend(_tokens(tokenizer, "\nALLOWED OPTIONS:\n"))
121
 
@@ -513,3 +518,59 @@ def load_release_model(
513
  head = head.to(device=device, dtype=dtype)
514
  processor = AutoProcessor.from_pretrained(path)
515
  return ClefModel(backbone, head).eval(), processor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  and ``questions``. Each question has a ``type`` (``noul``, ``choice``, or ``score``),
5
  ``instructions``, and, for ``choice`` and ``score``, ``criteria`` describing the
6
  allowed options. The model returns one logit per allowed option for every question.
7
+ ``systemone`` answers a Jev/SystemOne ``/v1/systemone`` request body with the same
8
+ response body.
9
  """
10
 
11
  from __future__ import annotations
 
117
  )
118
  )
119
  question_start = len(schema_ids)
120
+ instructions = question.get("instructions")
121
+ if instructions is None or instructions == "":
122
+ instructions = str(question_id)
123
+ schema_ids.extend(_tokens(tokenizer, render(instructions)))
124
  question_end = len(schema_ids)
125
  schema_ids.extend(_tokens(tokenizer, "\nALLOWED OPTIONS:\n"))
126
 
 
518
  head = head.to(device=device, dtype=dtype)
519
  processor = AutoProcessor.from_pretrained(path)
520
  return ClefModel(backbone, head).eval(), processor
521
+
522
+
523
+ def systemone_answer(question: dict[str, Any], probabilities: dict[str, float]) -> dict[str, Any]:
524
+ """Convert per-option probabilities for one question into a SystemOne answer."""
525
+ if question["type"] == "noul":
526
+ return {"type": "noul", "noul": round(probabilities["true"], 4)}
527
+ if question["type"] == "choice":
528
+ options = [str(option) for option in question["criteria"]]
529
+ choice = max(options, key=probabilities.__getitem__)
530
+ return {
531
+ "type": "choice",
532
+ "choice": choice,
533
+ "confidence": round(probabilities[choice], 4),
534
+ "probabilities": {option: round(probabilities[option], 4) for option in options},
535
+ }
536
+ levels = [str(index) for index in range(len(question["criteria"]))]
537
+ return {
538
+ "type": "score",
539
+ "score": round(sum(index * probabilities[level] for index, level in enumerate(levels)), 4),
540
+ "confidence": round(max(probabilities[level] for level in levels), 4),
541
+ "legend": dict(zip(levels, question["criteria"])),
542
+ "probabilities": {level: round(probabilities[level], 4) for level in levels},
543
+ }
544
+
545
+
546
+ @torch.inference_mode()
547
+ def systemone(model: ClefModel, processor: Any, request: dict[str, Any], max_length: int = 16384) -> dict[str, Any]:
548
+ """Answer a Jev/SystemOne ``/v1/systemone`` request body with a SystemOne response body.
549
+
550
+ The request has ``model``, ``state``, and ``questions``, plus optional ``images`` and ``videos``.
551
+ """
552
+ questions = request.get("questions")
553
+ if not isinstance(request.get("model"), str) or "state" not in request:
554
+ raise ValueError("model and state are required")
555
+ if not isinstance(questions, dict) or not questions:
556
+ raise ValueError("at least one question is required")
557
+ for question_id, question in questions.items():
558
+ if question.get("type") not in QUESTION_TYPES:
559
+ raise ValueError(f"{question_id}: type must be noul, choice, or score")
560
+ if question["type"] != "noul" and not question.get("criteria"):
561
+ raise ValueError(f"{question_id}: criteria must not be empty")
562
+ encoded = encode_record(processor.tokenizer, request, max_length=max_length, processor=processor)
563
+ device = next(model.parameters()).device
564
+ logits = model(collate_records([encoded], processor.tokenizer.pad_token_id, device))[0]
565
+ answers = {
566
+ question.question_id: systemone_answer(
567
+ questions[question.question_id],
568
+ dict(zip(question.option_ids, question_logits.float().softmax(-1).tolist())),
569
+ )
570
+ for question, question_logits in zip(encoded.questions, logits)
571
+ }
572
+ return {
573
+ "model": request["model"],
574
+ "answers": answers,
575
+ "usage": {"input_tokens": len(encoded.input_ids), "output_tokens": 0},
576
+ }