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.
- sys1_decision_guard-0.1.0/PKG-INFO +148 -0
- sys1_decision_guard-0.1.0/README.md +133 -0
- sys1_decision_guard-0.1.0/pyproject.toml +28 -0
- sys1_decision_guard-0.1.0/setup.cfg +4 -0
- sys1_decision_guard-0.1.0/src/decision_guard/__init__.py +5 -0
- sys1_decision_guard-0.1.0/src/decision_guard/adapters/__init__.py +6 -0
- sys1_decision_guard-0.1.0/src/decision_guard/adapters/base.py +20 -0
- sys1_decision_guard-0.1.0/src/decision_guard/adapters/jev.py +76 -0
- sys1_decision_guard-0.1.0/src/decision_guard/adapters/laya.py +137 -0
- sys1_decision_guard-0.1.0/src/decision_guard/calibration.py +124 -0
- sys1_decision_guard-0.1.0/src/decision_guard/safety.py +91 -0
- sys1_decision_guard-0.1.0/src/decision_guard/schema.py +52 -0
- sys1_decision_guard-0.1.0/src/decision_guard/store.py +100 -0
- sys1_decision_guard-0.1.0/src/decision_guard/thresholds.py +107 -0
- sys1_decision_guard-0.1.0/src/sys1_decision_guard.egg-info/PKG-INFO +148 -0
- sys1_decision_guard-0.1.0/src/sys1_decision_guard.egg-info/SOURCES.txt +20 -0
- sys1_decision_guard-0.1.0/src/sys1_decision_guard.egg-info/dependency_links.txt +1 -0
- sys1_decision_guard-0.1.0/src/sys1_decision_guard.egg-info/requires.txt +8 -0
- sys1_decision_guard-0.1.0/src/sys1_decision_guard.egg-info/top_level.txt +1 -0
- sys1_decision_guard-0.1.0/tests/test_calibration.py +60 -0
- sys1_decision_guard-0.1.0/tests/test_safety.py +55 -0
- sys1_decision_guard-0.1.0/tests/test_thresholds.py +54 -0
|
@@ -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,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)
|