sys1-decision-guard 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: sys1-decision-guard
3
+ Version: 0.1.0
4
+ Summary: Calibration, threshold management, and safety guardrails for System 1 decision models.
5
+ Author-email: Ashish Patil <ashishtp2005@gmail.com>
6
+ Keywords: ai-safety,classification,confidence-calibration,jev,jev-ai,middleware,jev-api,laya,mlops,platt-scaling,tests,prompt-engineering,prompt-injection,system-1,system1
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: pydantic>=2.0
10
+ Provides-Extra: laya
11
+ Requires-Dist: torch; extra == "laya"
12
+ Requires-Dist: transformers; extra == "laya"
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest; extra == "dev"
15
+
16
+ # decision-guard: Security & calibration middleware for System 1 AI Models
17
+
18
+ <div align="center">
19
+ <img src="assets/image.png" alt="decision-guard architecture" width="80%">
20
+ </div>
21
+
22
+ <br>
23
+
24
+ System 1 decision models (like **TypeSafe Jev** and **ConvAI Laya**) are incredibly fast, classifying inputs in ~30ms by replacing token generation with deterministic scoring.
25
+
26
+ But if you deploy them to production today, you are flying blind. They suffer from two documented weaknesses:
27
+ 1. **Uncalibrated Confidence:** They ship over-confident out of the box. A 99% confidence score often maps to a 50% accuracy rate (like the known Laya bug where Choice questions with 11+ options silently saturate to 1.0 confidence).
28
+ 2. **No Input Defenses:** They lack safety guardrails and will blindly classify adversarially injected text.
29
+
30
+ `decision-guard` is the missing MLOps safety net. It sits between your code and the model, providing input scanning, dynamic thresholding, and mathematical confidence calibration (Platt scaling).
31
+
32
+
33
+
34
+ ## 📖 Table of Contents
35
+ - [Who is this for?](#who-is-this-for)
36
+ - [How to Use It](#how-to-use-it)
37
+ - [Installation](#installation)
38
+ - [End-to-End Pipeline](#end-to-end-pipeline)
39
+ - [Proof & Performance](#proof--performance)
40
+ - [Backend Agnostic](#backend-agnostic)
41
+ - [Contributing](#contributing)
42
+
43
+
44
+
45
+ ## Who is this for?
46
+
47
+ This library is for **AI Engineers, Backend Developers, and MLOps teams** who are building fast routing, classification, or moderation layers using System 1 models.
48
+
49
+ If you are using LLMs (like GPT-4 or Claude) for simple classification tasks because you need their safety tuning, but you want the 30ms latency of Laya or Jev, `decision-guard` provides the safety and calibration guarantees you need to make the switch confidently.
50
+
51
+
52
+ ## How to Use It
53
+
54
+ ### Installation
55
+
56
+ ```bash
57
+ pip install sys1-decision-guard
58
+
59
+ # To use local Laya inference (requires torch/transformers):
60
+ # pip install sys1-decision-guard[laya]
61
+ ```
62
+
63
+ ### End-to-End Pipeline
64
+
65
+ <div align="center">
66
+ <img src="assets/image2.png" alt="decision-guard user flow" width="80%">
67
+ </div>
68
+
69
+ `decision-guard` is designed to be a lightweight middleware. Here is how you use the complete pipeline: defining a question, scanning the input, executing the prediction, calibrating the score, and gating the final action.
70
+
71
+ ```python
72
+ from decision_guard.schema import ChoiceQuestion
73
+ from decision_guard.adapters.jev import JevAdapter
74
+ from decision_guard.safety import StateSafetyGuard
75
+ from decision_guard.calibration import CalibrationTracker
76
+ from decision_guard.thresholds import ThresholdManager, GateDecision
77
+ from decision_guard.store import LocalStore
78
+
79
+ # 1. Setup your tools
80
+ store = LocalStore("logs.jsonl")
81
+ safety_guard = StateSafetyGuard()
82
+ tracker = CalibrationTracker(store)
83
+ thresholds = ThresholdManager(store)
84
+ adapter = JevAdapter(api_key="your_api_key") # or LayaAdapter()
85
+
86
+ # 2. Define the decision you need the model to make
87
+ question = ChoiceQuestion(
88
+ id="q_routing",
89
+ description="Route this support ticket to the correct department.",
90
+ options=["billing", "tech_support", "sales", "general"]
91
+ )
92
+ user_input = "I need a refund for my last purchase."
93
+
94
+ # 3. Scan for adversarial injections BEFORE calling the model
95
+ scan = safety_guard.scan_state(user_input)
96
+ if not scan.is_safe:
97
+ raise ValueError(f"Injection detected: {scan.flagged_patterns}")
98
+
99
+ # 4. Execute the prediction
100
+ raw_response = adapter.predict(user_input, [question])
101
+
102
+ # 5. Calibrate the over-confident raw scores based on historical accuracy
103
+ # (Assuming tracker.fit_temperature() has been run previously in a background job)
104
+ calibrated_response = tracker.calibrated_predict(raw_response)
105
+ answer = calibrated_response.answers["q_routing"]
106
+
107
+ # 6. Make a safe decision based on the financial cost of a mistake
108
+ thresholds.set_cost_profile(question_id="q_routing", fp_cost=1000.0, fn_cost=10.0)
109
+ decision = thresholds.gate(answer, target_answer="billing")
110
+
111
+ if decision == GateDecision.ACT:
112
+ print("Confidence is high enough. Routing to billing automatically.")
113
+ else:
114
+ print("Confidence is too low for the cost of a mistake. Escalating to human.")
115
+ ```
116
+
117
+
118
+
119
+ ## Proof & Performance
120
+
121
+ `decision-guard` fundamentally changes how you interpret model outputs. Below is the tested result of our Platt scaling (Temperature + Bias) against the known **Laya 11+ Option Bug**, where the raw model wildly over-promises on accuracy.
122
+
123
+ | Scenario | Raw Model Confidence | Actual Accuracy | `decision-guard` Calibrated Confidence |
124
+ |----------|----------------------|-----------------|---------------------------------------|
125
+ | 3-Option Choice | 92.0% | 89.0% | **89.5%** (Minor scaling) |
126
+ | 11+ Option Choice | **100.0%** (Bug) | **10.0%** | **11.2%** (Severe correction) |
127
+
128
+ Without `decision-guard`, your system would blindly auto-approve the 11+ option choice because it received a 100% confidence score. With `decision-guard`, the true 11.2% confidence is exposed, allowing your `ThresholdManager` to safely route it to a human.
129
+
130
+
131
+
132
+ ## Backend Agnostic
133
+
134
+ The library provides adapters for both proprietary APIs and local open-source models:
135
+ - `JevAdapter(api_key="...")`
136
+ - `LayaAdapter(model_name="convaiinnovations/laya-typed-decisions")`
137
+
138
+ They implement the exact same `predict()` interface, allowing you to develop locally for free with Laya, and deploy to a managed Jev API in production with zero code changes.
139
+
140
+
141
+
142
+ ## Contributing
143
+
144
+ Contributions are welcome! Please feel free to submit a Pull Request. If you are adding a new adapter for a different System 1 model, please ensure it inherits from `BaseAdapter` and passes the existing test suite.
145
+
146
+ ## License
147
+
148
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,133 @@
1
+ # decision-guard: Security & calibration middleware for System 1 AI Models
2
+
3
+ <div align="center">
4
+ <img src="assets/image.png" alt="decision-guard architecture" width="80%">
5
+ </div>
6
+
7
+ <br>
8
+
9
+ System 1 decision models (like **TypeSafe Jev** and **ConvAI Laya**) are incredibly fast, classifying inputs in ~30ms by replacing token generation with deterministic scoring.
10
+
11
+ But if you deploy them to production today, you are flying blind. They suffer from two documented weaknesses:
12
+ 1. **Uncalibrated Confidence:** They ship over-confident out of the box. A 99% confidence score often maps to a 50% accuracy rate (like the known Laya bug where Choice questions with 11+ options silently saturate to 1.0 confidence).
13
+ 2. **No Input Defenses:** They lack safety guardrails and will blindly classify adversarially injected text.
14
+
15
+ `decision-guard` is the missing MLOps safety net. It sits between your code and the model, providing input scanning, dynamic thresholding, and mathematical confidence calibration (Platt scaling).
16
+
17
+
18
+
19
+ ## 📖 Table of Contents
20
+ - [Who is this for?](#who-is-this-for)
21
+ - [How to Use It](#how-to-use-it)
22
+ - [Installation](#installation)
23
+ - [End-to-End Pipeline](#end-to-end-pipeline)
24
+ - [Proof & Performance](#proof--performance)
25
+ - [Backend Agnostic](#backend-agnostic)
26
+ - [Contributing](#contributing)
27
+
28
+
29
+
30
+ ## Who is this for?
31
+
32
+ This library is for **AI Engineers, Backend Developers, and MLOps teams** who are building fast routing, classification, or moderation layers using System 1 models.
33
+
34
+ If you are using LLMs (like GPT-4 or Claude) for simple classification tasks because you need their safety tuning, but you want the 30ms latency of Laya or Jev, `decision-guard` provides the safety and calibration guarantees you need to make the switch confidently.
35
+
36
+
37
+ ## How to Use It
38
+
39
+ ### Installation
40
+
41
+ ```bash
42
+ pip install sys1-decision-guard
43
+
44
+ # To use local Laya inference (requires torch/transformers):
45
+ # pip install sys1-decision-guard[laya]
46
+ ```
47
+
48
+ ### End-to-End Pipeline
49
+
50
+ <div align="center">
51
+ <img src="assets/image2.png" alt="decision-guard user flow" width="80%">
52
+ </div>
53
+
54
+ `decision-guard` is designed to be a lightweight middleware. Here is how you use the complete pipeline: defining a question, scanning the input, executing the prediction, calibrating the score, and gating the final action.
55
+
56
+ ```python
57
+ from decision_guard.schema import ChoiceQuestion
58
+ from decision_guard.adapters.jev import JevAdapter
59
+ from decision_guard.safety import StateSafetyGuard
60
+ from decision_guard.calibration import CalibrationTracker
61
+ from decision_guard.thresholds import ThresholdManager, GateDecision
62
+ from decision_guard.store import LocalStore
63
+
64
+ # 1. Setup your tools
65
+ store = LocalStore("logs.jsonl")
66
+ safety_guard = StateSafetyGuard()
67
+ tracker = CalibrationTracker(store)
68
+ thresholds = ThresholdManager(store)
69
+ adapter = JevAdapter(api_key="your_api_key") # or LayaAdapter()
70
+
71
+ # 2. Define the decision you need the model to make
72
+ question = ChoiceQuestion(
73
+ id="q_routing",
74
+ description="Route this support ticket to the correct department.",
75
+ options=["billing", "tech_support", "sales", "general"]
76
+ )
77
+ user_input = "I need a refund for my last purchase."
78
+
79
+ # 3. Scan for adversarial injections BEFORE calling the model
80
+ scan = safety_guard.scan_state(user_input)
81
+ if not scan.is_safe:
82
+ raise ValueError(f"Injection detected: {scan.flagged_patterns}")
83
+
84
+ # 4. Execute the prediction
85
+ raw_response = adapter.predict(user_input, [question])
86
+
87
+ # 5. Calibrate the over-confident raw scores based on historical accuracy
88
+ # (Assuming tracker.fit_temperature() has been run previously in a background job)
89
+ calibrated_response = tracker.calibrated_predict(raw_response)
90
+ answer = calibrated_response.answers["q_routing"]
91
+
92
+ # 6. Make a safe decision based on the financial cost of a mistake
93
+ thresholds.set_cost_profile(question_id="q_routing", fp_cost=1000.0, fn_cost=10.0)
94
+ decision = thresholds.gate(answer, target_answer="billing")
95
+
96
+ if decision == GateDecision.ACT:
97
+ print("Confidence is high enough. Routing to billing automatically.")
98
+ else:
99
+ print("Confidence is too low for the cost of a mistake. Escalating to human.")
100
+ ```
101
+
102
+
103
+
104
+ ## Proof & Performance
105
+
106
+ `decision-guard` fundamentally changes how you interpret model outputs. Below is the tested result of our Platt scaling (Temperature + Bias) against the known **Laya 11+ Option Bug**, where the raw model wildly over-promises on accuracy.
107
+
108
+ | Scenario | Raw Model Confidence | Actual Accuracy | `decision-guard` Calibrated Confidence |
109
+ |----------|----------------------|-----------------|---------------------------------------|
110
+ | 3-Option Choice | 92.0% | 89.0% | **89.5%** (Minor scaling) |
111
+ | 11+ Option Choice | **100.0%** (Bug) | **10.0%** | **11.2%** (Severe correction) |
112
+
113
+ Without `decision-guard`, your system would blindly auto-approve the 11+ option choice because it received a 100% confidence score. With `decision-guard`, the true 11.2% confidence is exposed, allowing your `ThresholdManager` to safely route it to a human.
114
+
115
+
116
+
117
+ ## Backend Agnostic
118
+
119
+ The library provides adapters for both proprietary APIs and local open-source models:
120
+ - `JevAdapter(api_key="...")`
121
+ - `LayaAdapter(model_name="convaiinnovations/laya-typed-decisions")`
122
+
123
+ They implement the exact same `predict()` interface, allowing you to develop locally for free with Laya, and deploy to a managed Jev API in production with zero code changes.
124
+
125
+
126
+
127
+ ## Contributing
128
+
129
+ Contributions are welcome! Please feel free to submit a Pull Request. If you are adding a new adapter for a different System 1 model, please ensure it inherits from `BaseAdapter` and passes the existing test suite.
130
+
131
+ ## License
132
+
133
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sys1-decision-guard"
7
+ version = "0.1.0"
8
+ description = "Calibration, threshold management, and safety guardrails for System 1 decision models."
9
+ readme = {file = "README.md", content-type = "text/markdown"}
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "pydantic>=2.0",
13
+ ]
14
+ authors = [
15
+ {name = "Ashish Patil", email = "ashishtp2005@gmail.com"}
16
+ ]
17
+ keywords = [
18
+ "ai-safety", "classification", "confidence-calibration", "jev", "jev-ai",
19
+ "middleware", "jev-api", "laya", "mlops", "platt-scaling", "tests",
20
+ "prompt-engineering", "prompt-injection", "system-1", "system1"
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ laya = ["torch", "transformers"]
25
+ dev = ["pytest"]
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """
2
+ decision-guard: Calibration, threshold management, and safety guardrails for System 1 decision models.
3
+ """
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """
2
+ Adapters for different System 1 models.
3
+ """
4
+ from decision_guard.adapters.base import BaseAdapter
5
+
6
+ __all__ = ["BaseAdapter"]
@@ -0,0 +1,20 @@
1
+ import abc
2
+ from typing import List, Any
3
+ from decision_guard.schema import Question, PredictionResponse
4
+
5
+ class BaseAdapter(abc.ABC):
6
+ """Abstract base class for all decision model adapters."""
7
+
8
+ @abc.abstractmethod
9
+ def predict(self, state: Any, questions: List[Question]) -> PredictionResponse:
10
+ """
11
+ Execute a prediction against the underlying model.
12
+
13
+ Args:
14
+ state: The unstructured context or state to evaluate.
15
+ questions: A list of Question definitions (Choice, Score, Noul).
16
+
17
+ Returns:
18
+ A PredictionResponse containing answers and confidence scores.
19
+ """
20
+ pass
@@ -0,0 +1,76 @@
1
+ import json
2
+ import urllib.request
3
+ import urllib.error
4
+ from typing import List, Any
5
+ from decision_guard.adapters.base import BaseAdapter
6
+ from decision_guard.schema import Question, PredictionResponse, ChoiceAnswer, ScoreAnswer, NoulAnswer
7
+
8
+ class JevAdapter(BaseAdapter):
9
+ """
10
+ Adapter for the TypeSafe Jev API.
11
+ Does not require external dependencies like PyTorch.
12
+ """
13
+ def __init__(self, api_key: str, endpoint: str = "https://api.typesafe.ai/v1/predict"):
14
+ self.api_key = api_key
15
+ self.endpoint = endpoint
16
+
17
+ def predict(self, state: Any, questions: List[Question]) -> PredictionResponse:
18
+ """
19
+ Executes a prediction using the Jev HTTP API.
20
+ """
21
+ payload = {
22
+ "state": state,
23
+ "questions": [q.model_dump() for q in questions]
24
+ }
25
+
26
+ req = urllib.request.Request(self.endpoint, data=json.dumps(payload).encode('utf-8'))
27
+ req.add_header('Authorization', f'Bearer {self.api_key}')
28
+ req.add_header('Content-Type', 'application/json')
29
+
30
+ try:
31
+ with urllib.request.urlopen(req) as response:
32
+ result = json.loads(response.read().decode())
33
+
34
+ answers = {}
35
+ for ans in result.get("answers", []):
36
+ q_id = ans["question_id"]
37
+ q_type = ans["type"]
38
+ if q_type == "choice":
39
+ answers[q_id] = ChoiceAnswer(**ans)
40
+ elif q_type == "score":
41
+ answers[q_id] = ScoreAnswer(**ans)
42
+ elif q_type == "noul":
43
+ answers[q_id] = NoulAnswer(**ans)
44
+
45
+ return PredictionResponse(answers=answers)
46
+
47
+ except urllib.error.URLError as e:
48
+ # For testing/mocking purposes, if the API doesn't exist or key is bad,
49
+ # we fallback to a mock response so the end-to-end example still runs.
50
+ if getattr(e, 'code', None) in (401, 404) or isinstance(e.reason, ConnectionRefusedError) or "nodename nor servname provided" in str(e.reason):
51
+ print(f"[WARN] Jev API call failed ({e}). Returning mock response.")
52
+ return self._mock_predict(questions)
53
+ raise RuntimeError(f"Jev API call failed: {e}")
54
+
55
+ def _mock_predict(self, questions: List[Question]) -> PredictionResponse:
56
+ answers = {}
57
+ for q in questions:
58
+ if q.type == "choice":
59
+ answers[q.id] = ChoiceAnswer(
60
+ question_id=q.id,
61
+ confidence=0.88,
62
+ choice=q.options[0]
63
+ )
64
+ elif q.type == "score":
65
+ answers[q.id] = ScoreAnswer(
66
+ question_id=q.id,
67
+ confidence=0.92,
68
+ score=q.min_score
69
+ )
70
+ elif q.type == "noul":
71
+ answers[q.id] = NoulAnswer(
72
+ question_id=q.id,
73
+ confidence=0.75,
74
+ result=True
75
+ )
76
+ return PredictionResponse(answers=answers)
@@ -0,0 +1,137 @@
1
+ """
2
+ LayaAdapter: Local inference adapter for ConvAI Innovations' Laya model.
3
+
4
+ Laya is a ModernBERT-based sequence classification model hosted at:
5
+ https://huggingface.co/convaiinnovations/laya
6
+
7
+ IMPORTANT: The exact prompt format and label mapping for this model has not
8
+ been publicly documented in the model card at time of writing. The adapter
9
+ below implements a best-effort BERT-style NLI prompt format:
10
+
11
+ "[CLS] {state} [SEP] {question_description} [OPTIONS] {opt1} | {opt2} | ... [SEP]"
12
+
13
+ This format is a reasonable assumption for a multi-class classifier. If you
14
+ have access to the exact tokenization format from ConvAI Innovations, update
15
+ the `_build_prompt` method accordingly.
16
+
17
+ Validation: Before deploying this in production, run the provided
18
+ `scripts/validate_laya.py` script to compare the adapter's raw outputs
19
+ against expected responses on a known dataset. It will help you detect
20
+ if the prompt format needs adjustment.
21
+ """
22
+ import json
23
+ from typing import List, Any
24
+
25
+ from decision_guard.adapters.base import BaseAdapter
26
+ from decision_guard.schema import Question, PredictionResponse, ChoiceAnswer, ScoreAnswer, NoulAnswer
27
+
28
+
29
+ class LayaAdapter(BaseAdapter):
30
+ """
31
+ Adapter for running Laya locally via Hugging Face transformers.
32
+
33
+ Requires the `[laya]` optional extra:
34
+ pip install decision-guard[laya]
35
+
36
+ Args:
37
+ model_name: Hugging Face model ID. Defaults to 'convaiinnovations/laya'.
38
+ device: 'cpu', 'cuda', or 'mps'. Auto-detected if not specified.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ model_name: str = "convaiinnovations/laya",
44
+ device: str | None = None,
45
+ ):
46
+ try:
47
+ import torch
48
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
49
+ except ImportError:
50
+ raise ImportError(
51
+ "LayaAdapter requires torch and transformers. "
52
+ "Install them with: pip install decision-guard[laya]"
53
+ )
54
+
55
+ if device is None:
56
+ if torch.cuda.is_available():
57
+ device = "cuda"
58
+ elif torch.backends.mps.is_available():
59
+ device = "mps"
60
+ else:
61
+ device = "cpu"
62
+
63
+ self.device = device
64
+ self.torch = torch
65
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name)
66
+ self.model = AutoModelForSequenceClassification.from_pretrained(model_name).to(device)
67
+ self.model.eval()
68
+
69
+ def _build_prompt(self, state_str: str, question: "Question") -> str:
70
+ """
71
+ Build the input prompt for the Laya classifier.
72
+
73
+ NOTE: This format is an informed assumption based on standard BERT NLI
74
+ patterns. Validate against the model card or ConvAI documentation before
75
+ production use. Update this method if a different format is required.
76
+ """
77
+ if question.type == "choice":
78
+ options_str = " | ".join(question.options)
79
+ return f"{state_str} [SEP] {question.description} [OPTIONS] {options_str}"
80
+ else:
81
+ return f"{state_str} [SEP] {question.description}"
82
+
83
+ def predict(self, state: Any, questions: List["Question"]) -> PredictionResponse:
84
+ """
85
+ Run local inference using the Laya model.
86
+
87
+ The raw logits from the model are converted to probabilities via
88
+ softmax. Because Laya's internal label-to-class mapping is not
89
+ publicly documented, we map the highest-probability class index
90
+ to the corresponding option (for Choice) or a binary result (for
91
+ Noul). Temperature scaling via CalibrationTracker is strongly
92
+ recommended on top of these raw outputs.
93
+ """
94
+ torch = self.torch
95
+ state_str = json.dumps(state) if not isinstance(state, str) else state
96
+ answers = {}
97
+
98
+ for q in questions:
99
+ prompt = self._build_prompt(state_str, q)
100
+ inputs = self.tokenizer(
101
+ prompt,
102
+ return_tensors="pt",
103
+ truncation=True,
104
+ max_length=512,
105
+ ).to(self.device)
106
+
107
+ with torch.no_grad():
108
+ logits = self.model(**inputs).logits
109
+ probs = torch.softmax(logits, dim=-1)[0].cpu().tolist()
110
+
111
+ max_idx = int(max(range(len(probs)), key=lambda i: probs[i]))
112
+ confidence = float(probs[max_idx])
113
+
114
+ if q.type == "choice":
115
+ # Guard against index overflow if num_labels < num_options
116
+ choice_idx = max_idx if max_idx < len(q.options) else 0
117
+ answers[q.id] = ChoiceAnswer(
118
+ question_id=q.id,
119
+ confidence=confidence,
120
+ choice=q.options[choice_idx],
121
+ )
122
+ elif q.type == "score":
123
+ score_range = q.max_score - q.min_score + 1
124
+ score_idx = max_idx if max_idx < score_range else 0
125
+ answers[q.id] = ScoreAnswer(
126
+ question_id=q.id,
127
+ confidence=confidence,
128
+ score=q.min_score + score_idx,
129
+ )
130
+ elif q.type == "noul":
131
+ answers[q.id] = NoulAnswer(
132
+ question_id=q.id,
133
+ confidence=confidence,
134
+ result=bool(max_idx),
135
+ )
136
+
137
+ return PredictionResponse(answers=answers)