pydecide 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.
- pydecide-0.1.0/.github/workflows/ci.yml +15 -0
- pydecide-0.1.0/.gitignore +7 -0
- pydecide-0.1.0/CHANGELOG.md +29 -0
- pydecide-0.1.0/LICENSE +21 -0
- pydecide-0.1.0/PKG-INFO +379 -0
- pydecide-0.1.0/README.md +340 -0
- pydecide-0.1.0/decide/__init__.py +53 -0
- pydecide-0.1.0/decide/backends/__init__.py +67 -0
- pydecide-0.1.0/decide/backends/_http.py +99 -0
- pydecide-0.1.0/decide/backends/base.py +91 -0
- pydecide-0.1.0/decide/backends/crossencoder.py +416 -0
- pydecide-0.1.0/decide/backends/laya.py +176 -0
- pydecide-0.1.0/decide/backends/laya_mlx.py +64 -0
- pydecide-0.1.0/decide/backends/llm.py +358 -0
- pydecide-0.1.0/decide/backends/openrouter.py +12 -0
- pydecide-0.1.0/decide/backends/typesafe.py +82 -0
- pydecide-0.1.0/decide/cli.py +294 -0
- pydecide-0.1.0/decide/client.py +477 -0
- pydecide-0.1.0/decide/errors.py +71 -0
- pydecide-0.1.0/decide/gate.py +56 -0
- pydecide-0.1.0/decide/server.py +124 -0
- pydecide-0.1.0/decide/types.py +146 -0
- pydecide-0.1.0/decide/wire.py +327 -0
- pydecide-0.1.0/docs/superpowers/specs/2026-09-21-decide-design.md +217 -0
- pydecide-0.1.0/pyproject.toml +67 -0
- pydecide-0.1.0/scripts/dump_typesafe_schema.py +35 -0
- pydecide-0.1.0/tests/__init__.py +0 -0
- pydecide-0.1.0/tests/conftest.py +90 -0
- pydecide-0.1.0/tests/fixtures/typesafe_schema.json +605 -0
- pydecide-0.1.0/tests/test_backends.py +23 -0
- pydecide-0.1.0/tests/test_cli.py +297 -0
- pydecide-0.1.0/tests/test_client.py +513 -0
- pydecide-0.1.0/tests/test_crossencoder.py +177 -0
- pydecide-0.1.0/tests/test_gate.py +41 -0
- pydecide-0.1.0/tests/test_laya.py +342 -0
- pydecide-0.1.0/tests/test_llm.py +465 -0
- pydecide-0.1.0/tests/test_openrouter.py +22 -0
- pydecide-0.1.0/tests/test_schema_golden.py +77 -0
- pydecide-0.1.0/tests/test_server.py +420 -0
- pydecide-0.1.0/tests/test_types.py +65 -0
- pydecide-0.1.0/tests/test_typesafe.py +189 -0
- pydecide-0.1.0/tests/test_wire.py +445 -0
- pydecide-0.1.0/uv.lock +2468 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
jobs:
|
|
4
|
+
test:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
strategy:
|
|
7
|
+
matrix: { python: ["3.10", "3.11", "3.12", "3.13", "3.14"] }
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
- uses: astral-sh/setup-uv@v3
|
|
11
|
+
- run: uv python install ${{ matrix.python }}
|
|
12
|
+
- run: uv sync --python ${{ matrix.python }}
|
|
13
|
+
- run: uv run ruff check .
|
|
14
|
+
- run: uv run ruff format --check .
|
|
15
|
+
- run: uv run pytest
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-09-22)
|
|
4
|
+
|
|
5
|
+
- Add `Choice`, `Score`, `Noul`, `Request`, `Response` and their answer types
|
|
6
|
+
as frozen, validated dataclasses (`decide/types.py`).
|
|
7
|
+
- Add `Client`/`AsyncClient` with an ordered backend fallback chain,
|
|
8
|
+
confidence gating via `Gate`, `Client.from_env`/`AsyncClient.from_env`
|
|
9
|
+
environment-based configuration, and `decide_batch` with partial-failure
|
|
10
|
+
reporting through `AllBackendsFailed`.
|
|
11
|
+
- Add six backends: `typesafe` (TypeSafe/Jev HTTP API), `openrouter`
|
|
12
|
+
(OpenRouter Decisions endpoint), `laya` (local PyTorch), `laya_mlx`
|
|
13
|
+
(local MLX, Apple Silicon), `crossencoder` (any sentence-transformers
|
|
14
|
+
CrossEncoder, with generic and KaLM-Jev templates), and `llm` (JSON-prompted
|
|
15
|
+
fallback over any OpenAI-compatible chat-completions server).
|
|
16
|
+
- Add a Jev/TypeSafe wire-compatible HTTP server (`decide serve`, the
|
|
17
|
+
`server` extra) exposing `POST /v1/systemone`, `GET /health` and
|
|
18
|
+
`GET /v1/models` over any configured backend chain.
|
|
19
|
+
- Add a command line interface: `decide ask`, `decide backends`, `decide serve`.
|
|
20
|
+
|
|
21
|
+
### Known limitations
|
|
22
|
+
|
|
23
|
+
- LLM emulation (`llm` backend) probabilities are self-reported by the
|
|
24
|
+
model, not measured or calibrated.
|
|
25
|
+
- CrossEncoder (`crossencoder` backend) outputs are normalized scores
|
|
26
|
+
(softmax/sigmoid over raw logits), not calibrated probabilities.
|
|
27
|
+
- `AsyncClient.decide_batch` calls each backend's `adecide` concurrently
|
|
28
|
+
per request; it does not use a backend's batch path.
|
|
29
|
+
- The `mlx` extra (`laya_mlx` backend) needs Python 3.11 or newer.
|
pydecide-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gopalji Gaur
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pydecide-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pydecide
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One client for every decision model: Choice, Score and Noul over Jev, OpenRouter, laya, MLX, CrossEncoders and an LLM fallback.
|
|
5
|
+
Project-URL: Homepage, https://github.com/gopaljigaur/decide
|
|
6
|
+
Project-URL: Issues, https://github.com/gopaljigaur/decide/issues
|
|
7
|
+
Author: Gopalji Gaur
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: classification,decision model,jev,laya,routing,system one,typesafe
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: fastapi>=0.110; extra == 'all'
|
|
23
|
+
Requires-Dist: laya-mlx>=0.1; (python_version >= '3.11') and extra == 'all'
|
|
24
|
+
Requires-Dist: laya>=0.3; extra == 'all'
|
|
25
|
+
Requires-Dist: sentence-transformers>=3.0; extra == 'all'
|
|
26
|
+
Requires-Dist: torch; extra == 'all'
|
|
27
|
+
Requires-Dist: uvicorn>=0.29; extra == 'all'
|
|
28
|
+
Provides-Extra: laya
|
|
29
|
+
Requires-Dist: laya>=0.3; extra == 'laya'
|
|
30
|
+
Provides-Extra: mlx
|
|
31
|
+
Requires-Dist: laya-mlx>=0.1; (python_version >= '3.11') and extra == 'mlx'
|
|
32
|
+
Provides-Extra: server
|
|
33
|
+
Requires-Dist: fastapi>=0.110; extra == 'server'
|
|
34
|
+
Requires-Dist: uvicorn>=0.29; extra == 'server'
|
|
35
|
+
Provides-Extra: st
|
|
36
|
+
Requires-Dist: sentence-transformers>=3.0; extra == 'st'
|
|
37
|
+
Requires-Dist: torch; extra == 'st'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# pydecide
|
|
41
|
+
|
|
42
|
+
[](https://github.com/gopaljigaur/decide/actions/workflows/ci.yml)
|
|
43
|
+
[](https://pypi.org/project/pydecide/)
|
|
44
|
+
|
|
45
|
+
`decide` is one Python client for typed decisions - Choice, Score and Noul -
|
|
46
|
+
over any "System One" decision model: TypeSafe's hosted Jev, OpenRouter's
|
|
47
|
+
Decisions endpoint, the open-weight laya family (PyTorch and MLX), any
|
|
48
|
+
sentence-transformers CrossEncoder, and a JSON-prompted LLM fallback. A
|
|
49
|
+
`Client` takes an ordered list of backends and a confidence policy: if a
|
|
50
|
+
backend errors or answers with low confidence, the next backend is tried.
|
|
51
|
+
The library also ships a small HTTP server that speaks TypeSafe's wire
|
|
52
|
+
protocol, so existing TypeSafe clients can point at a local model instead.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install pydecide
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Local model backends and the server are optional extras:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install "pydecide[server]" # decide serve (FastAPI + uvicorn)
|
|
64
|
+
pip install "pydecide[laya]" # laya backend (PyTorch)
|
|
65
|
+
pip install "pydecide[mlx]" # laya_mlx backend (Apple Silicon; needs Python 3.11+)
|
|
66
|
+
pip install "pydecide[st]" # crossencoder backend (sentence-transformers)
|
|
67
|
+
pip install "pydecide[all]" # everything above
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The `mlx` extra depends on `laya-mlx`, which requires Python 3.11 or newer;
|
|
71
|
+
on 3.10 the extra installs nothing and the `laya_mlx` backend is
|
|
72
|
+
unavailable.
|
|
73
|
+
|
|
74
|
+
## Quickstart: the fallback chain
|
|
75
|
+
|
|
76
|
+
`Client.from_env()` builds a backend chain from whatever is installed and
|
|
77
|
+
configured in the process environment. Pass an explicit `env=` mapping
|
|
78
|
+
instead (e.g. in tests) to configure from something other than
|
|
79
|
+
`os.environ`:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from decide import Client, Choice, Score, Noul
|
|
83
|
+
|
|
84
|
+
client = Client.from_env()
|
|
85
|
+
|
|
86
|
+
r = client.decide(
|
|
87
|
+
state={"ticket": "I was charged twice, please refund."},
|
|
88
|
+
questions={
|
|
89
|
+
"team": Choice(
|
|
90
|
+
"Which team handles this?",
|
|
91
|
+
{
|
|
92
|
+
"billing": "charges, refunds",
|
|
93
|
+
"engineering": "bugs and outages",
|
|
94
|
+
"sales": "pricing and upgrades",
|
|
95
|
+
},
|
|
96
|
+
),
|
|
97
|
+
"severity": Score("How severe is this?", ["minor", "degraded", "blocked"]),
|
|
98
|
+
"refund": Noul("Does the customer ask for money back?"),
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
r.choices["team"].choice # "billing"
|
|
103
|
+
r.choices["team"].probabilities # {"billing": 1.0, "engineering": 0.0, "sales": 0.0}
|
|
104
|
+
r.scores["severity"].score # 1.5522 (expected level index, 0..len(levels)-1)
|
|
105
|
+
r.scores["severity"].probabilities # [0.0197, 0.4083, 0.5719]
|
|
106
|
+
r.nouls["refund"].noul # 0.9461
|
|
107
|
+
r.meta.backend # "laya_mlx"
|
|
108
|
+
r.meta.latency_ms # 47.9
|
|
109
|
+
r.meta.route # ["laya_mlx:ok"]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This was run against the `laya_mlx` backend
|
|
113
|
+
(`DECIDE_LOCAL_MODEL=aac6fef/laya-multilingual-mlx`); every value above is
|
|
114
|
+
the real output of that run, not illustrative.
|
|
115
|
+
|
|
116
|
+
`from_env` picks the chain in this order, using whatever is both installed
|
|
117
|
+
and configured: `laya_mlx` or `laya` (if `DECIDE_LOCAL_MODEL` is set),
|
|
118
|
+
`typesafe` (if `TYPESAFE_API_KEY` is set), `openrouter` (if
|
|
119
|
+
`OPENROUTER_API_KEY` is set), `llm` (if `DECIDE_LLM_BASE_URL` or
|
|
120
|
+
`OPENAI_API_KEY` is set). Set `DECIDE_BACKENDS="laya,typesafe"` to override
|
|
121
|
+
the order explicitly. If nothing is configured, `from_env` raises
|
|
122
|
+
`ConfigError` naming every variable it checked.
|
|
123
|
+
|
|
124
|
+
`AsyncClient` has the same surface, `await`ed: `await client.decide(...)`,
|
|
125
|
+
`await client.decide_batch(...)`, `AsyncClient.from_env(...)`.
|
|
126
|
+
|
|
127
|
+
## Backends
|
|
128
|
+
|
|
129
|
+
| Module | Backend name | Extra | Notes |
|
|
130
|
+
|---|---|---|---|
|
|
131
|
+
| `typesafe.py` | `typesafe` | none (httpx only) | `POST {base_url}/v1/systemone`, bearer auth. Default base URL `https://api.typesafe.ai`, default model `jev-latest`. |
|
|
132
|
+
| `openrouter.py` | `openrouter` | none | Same wire shape as `typesafe`, `POST https://openrouter.ai/api/alpha/decisions`, default model `typesafe/jev-latest`. |
|
|
133
|
+
| `laya.py` | `laya` | `pydecide[laya]` | Local PyTorch `laya.Agent`, default model `convaiinnovations/laya` when constructed directly. |
|
|
134
|
+
| `laya_mlx.py` | `laya_mlx` | `pydecide[mlx]` (Python 3.11+) | Local MLX `laya_mlx.Agent` (Apple Silicon), default model `aac6fef/laya-multilingual-mlx` when constructed directly. |
|
|
135
|
+
| `crossencoder.py` | `crossencoder` | `pydecide[st]` | Local `sentence_transformers.CrossEncoder`. Not configurable from the environment; construct it directly and pass it to `Client([...])`. |
|
|
136
|
+
| `llm.py` | `llm` | none (httpx only) | Any OpenAI-compatible chat-completions server, default base URL `https://api.openai.com/v1`, default model `gpt-4o-mini`. The least trustworthy backend (see below). |
|
|
137
|
+
|
|
138
|
+
`crossencoder` is built by hand, for example:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from decide import Client, Choice
|
|
142
|
+
from decide.backends.crossencoder import CrossEncoderBackend
|
|
143
|
+
|
|
144
|
+
backend = CrossEncoderBackend("cross-encoder/ms-marco-MiniLM-L6-v2")
|
|
145
|
+
client = Client([backend])
|
|
146
|
+
|
|
147
|
+
r = client.decide(
|
|
148
|
+
"I was charged twice for the same order last week, please refund the duplicate charge.",
|
|
149
|
+
{
|
|
150
|
+
"team": Choice(
|
|
151
|
+
"Which team should handle this?",
|
|
152
|
+
{
|
|
153
|
+
"billing": "Charges, invoices, payment problems, refunds",
|
|
154
|
+
"eng": "Bugs, crashes, broken features",
|
|
155
|
+
"shipping": "Delivery status, delays, lost packages",
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
r.choices["team"].choice # "billing"
|
|
161
|
+
r.choices["team"].probabilities # {"billing": 0.947, "eng": 0.019, "shipping": 0.034}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Environment variables
|
|
165
|
+
|
|
166
|
+
| Variable | Backend | Meaning |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `DECIDE_LOCAL_MODEL` | `laya`, `laya_mlx` | Hugging Face repo id (or local path) of the model to load. Required to auto-select either backend. |
|
|
169
|
+
| `TYPESAFE_API_KEY` | `typesafe` | API key, sent as `Authorization: Bearer`. Required to auto-select `typesafe`. |
|
|
170
|
+
| `TYPESAFE_BASE_URL` | `typesafe` | Overrides the default `https://api.typesafe.ai`. |
|
|
171
|
+
| `OPENROUTER_API_KEY` | `openrouter` | API key, sent as `Authorization: Bearer`. Required to auto-select `openrouter`. |
|
|
172
|
+
| `DECIDE_LLM_BASE_URL` | `llm` | Base URL of an OpenAI-compatible chat-completions server. Setting it (or `OPENAI_API_KEY`) auto-selects `llm`. |
|
|
173
|
+
| `OPENAI_API_KEY` | `llm` | API key, sent as `Authorization: Bearer`, if the server needs one. |
|
|
174
|
+
| `DECIDE_LLM_MODEL` | `llm` | Model name to request; defaults to `gpt-4o-mini`. |
|
|
175
|
+
| `DECIDE_BACKENDS` | `Client.from_env` | Comma-separated backend names, overriding auto-detection entirely. |
|
|
176
|
+
| `DECIDE_MIN_CONFIDENCE` | `Client.from_env` (`Gate`) | Float threshold for the default `Gate` built by `from_env`, when no explicit `policy` is passed. |
|
|
177
|
+
| `DECIDE_API_KEY` | `decide serve` | Bearer token required to call the server, when `--api-key` is not passed. The flag takes precedence over this variable. |
|
|
178
|
+
|
|
179
|
+
## Gating and fallback
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from decide import Gate
|
|
183
|
+
|
|
184
|
+
Gate(
|
|
185
|
+
min_confidence=0.0, # top probability of a Choice, max(noul, 1-noul) for a Noul
|
|
186
|
+
per_question=None, # optional {"question_name": threshold} overrides
|
|
187
|
+
on_error="next", # or "raise" to stop the chain on the first BackendError
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
For each backend in order: call it. On `BackendError` with `on_error="next"`,
|
|
192
|
+
append `"<name>:error"` to `meta.route` and try the next backend (with
|
|
193
|
+
`on_error="raise"`, the error propagates immediately instead). If the
|
|
194
|
+
response fails the gate, append `"<name>:low_confidence:<question>=<value><threshold>"`
|
|
195
|
+
and try the next backend. If it passes, append `"<name>:ok"` and return.
|
|
196
|
+
Score answers are never gated (there is no single confidence number for an
|
|
197
|
+
expected value over levels).
|
|
198
|
+
|
|
199
|
+
If every backend is exhausted without a passing response, the best response
|
|
200
|
+
seen so far (highest minimum confidence across its gated answers) is
|
|
201
|
+
returned, with `meta.route[-1] == "<name>:accepted_low_confidence"`. If no
|
|
202
|
+
backend produced any response at all, `Client.decide` raises
|
|
203
|
+
`AllBackendsFailed(route, errors)`.
|
|
204
|
+
|
|
205
|
+
Route strings you will see in `meta.route`:
|
|
206
|
+
|
|
207
|
+
- `"<name>:ok"` - the backend answered and passed the gate.
|
|
208
|
+
- `"<name>:error"` - the backend raised a `BackendError`.
|
|
209
|
+
- `"<name>:low_confidence:<question>=<value><threshold>"` - the backend
|
|
210
|
+
answered but at least one gated question fell below its threshold.
|
|
211
|
+
- `"<name>:accepted_low_confidence"` - appended once, at the end of the
|
|
212
|
+
route, when no backend passed the gate and the best low-confidence
|
|
213
|
+
response was returned instead.
|
|
214
|
+
|
|
215
|
+
`Client.decide_batch`/`AsyncClient.decide_batch` run the same chain per
|
|
216
|
+
input state, preserving input order; a state that clears the gate on an
|
|
217
|
+
earlier backend is not sent to later ones. `Client.decide_batch` uses a
|
|
218
|
+
backend's real batch path when `capabilities().batch` is true, looping
|
|
219
|
+
`decide` per request otherwise. `AsyncClient.decide_batch` always loops
|
|
220
|
+
`adecide` per state, concurrently via `asyncio.gather`; it does not use a
|
|
221
|
+
backend's batch path in v1. If any state in the batch is left unrouted,
|
|
222
|
+
`AllBackendsFailed` carries `partial` (every `Response` that did resolve,
|
|
223
|
+
keyed by input index) and `failed` (the route so far for every state that
|
|
224
|
+
did not), so the resolved siblings are not silently lost.
|
|
225
|
+
|
|
226
|
+
## Server: point TypeSafe's SDK at a local model
|
|
227
|
+
|
|
228
|
+
Requires the `server` extra:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
pip install "pydecide[server]"
|
|
232
|
+
DECIDE_LOCAL_MODEL=aac6fef/laya-multilingual-mlx decide serve --backends laya_mlx --port 8811
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`GET /health` reports liveness and the configured backend names;
|
|
236
|
+
`GET /v1/models` lists them in an OpenAI-style shape; `POST /v1/systemone`
|
|
237
|
+
takes a TypeSafe `SystemOneRequest` body and returns a
|
|
238
|
+
`SystemOneResponse`-shaped body plus a `decide` extension carrying our own
|
|
239
|
+
backend/route metadata. Errors come back as
|
|
240
|
+
`{"error": {"message": ..., "type": ...}}` with a matching HTTP status.
|
|
241
|
+
|
|
242
|
+
If `--api-key`/`DECIDE_API_KEY` is set, requests must send a matching
|
|
243
|
+
`Authorization: Bearer <token>` header; the key must be ASCII, since HTTP
|
|
244
|
+
header bytes are latin-1 decoded by the server before comparison.
|
|
245
|
+
|
|
246
|
+
A raw request against the running server above:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
curl -s -X POST http://127.0.0.1:8811/v1/systemone \
|
|
250
|
+
-H "Content-Type: application/json" \
|
|
251
|
+
-d '{
|
|
252
|
+
"state": {"ticket": "I was charged twice, please refund."},
|
|
253
|
+
"questions": {
|
|
254
|
+
"team": {"type": "choice", "instructions": "Which team handles this?",
|
|
255
|
+
"criteria": {"billing": "charges, refunds", "engineering": "bugs and outages", "sales": "pricing and upgrades"}},
|
|
256
|
+
"severity": {"type": "score", "instructions": "How severe is this?", "criteria": ["minor", "degraded", "blocked"]},
|
|
257
|
+
"refund": {"type": "noul", "instructions": "Does the customer ask for money back?"}
|
|
258
|
+
}
|
|
259
|
+
}'
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"model": "aac6fef/laya-multilingual-mlx",
|
|
265
|
+
"answers": {
|
|
266
|
+
"team": {"type": "choice", "choice": "billing", "confidence": 1.0,
|
|
267
|
+
"probabilities": {"billing": 1.0, "engineering": 0.0, "sales": 0.0}},
|
|
268
|
+
"severity": {"type": "score", "score": 1.5522, "confidence": 0.5719,
|
|
269
|
+
"legend": {"0": "minor", "1": "degraded", "2": "blocked"},
|
|
270
|
+
"probabilities": {"0": 0.0197, "1": 0.4083, "2": 0.5719}},
|
|
271
|
+
"refund": {"type": "noul", "noul": 0.9461}
|
|
272
|
+
},
|
|
273
|
+
"usage": {"input_tokens": 0, "output_tokens": 0},
|
|
274
|
+
"decide": {"backend": "laya_mlx", "latency_ms": 50.5, "route": ["laya_mlx:ok"]}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
And the same request through TypeSafe's own SDK, pointed at the local
|
|
279
|
+
server (no API key is needed since this server has none configured, but the
|
|
280
|
+
SDK requires a non-empty string):
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from typesafe_sdk import TypeSafeClient, Choice, Score, Noul
|
|
284
|
+
|
|
285
|
+
client = TypeSafeClient(api_key="anything", base_url="http://127.0.0.1:8811")
|
|
286
|
+
|
|
287
|
+
resp = client.system_one(
|
|
288
|
+
state={"ticket": "I was charged twice, please refund."},
|
|
289
|
+
questions={
|
|
290
|
+
"team": Choice(
|
|
291
|
+
instructions="Which team handles this?",
|
|
292
|
+
criteria={
|
|
293
|
+
"billing": "charges, refunds",
|
|
294
|
+
"engineering": "bugs and outages",
|
|
295
|
+
"sales": "pricing and upgrades",
|
|
296
|
+
},
|
|
297
|
+
),
|
|
298
|
+
"severity": Score(
|
|
299
|
+
instructions="How severe is this?", criteria=["minor", "degraded", "blocked"]
|
|
300
|
+
),
|
|
301
|
+
"refund": Noul(instructions="Does the customer ask for money back?"),
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```text
|
|
307
|
+
model='aac6fef/laya-multilingual-mlx' usage=Usage(input_tokens=0, output_tokens=0)
|
|
308
|
+
answers={'team': ChoiceAnswer(type='choice', choice='billing', confidence=1.0,
|
|
309
|
+
probabilities={'billing': 1.0, 'engineering': 0.0, 'sales': 0.0}),
|
|
310
|
+
'severity': ScoreAnswer(type='score', score=1.5522, confidence=0.5719,
|
|
311
|
+
legend={0: 'minor', 1: 'degraded', 2: 'blocked'},
|
|
312
|
+
probabilities={0: 0.0197, 1: 0.4083, 2: 0.5719}),
|
|
313
|
+
'refund': NoulAnswer(type='noul', noul=0.9461)}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
TypeSafe's SDK parsed our server's response without modification: it is a
|
|
317
|
+
genuine `SystemOneResponse`, not a hand-shaped dict.
|
|
318
|
+
|
|
319
|
+
## CLI
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
decide ask "I was charged twice, please refund." \
|
|
323
|
+
--choice "team=billing,engineering,sales" \
|
|
324
|
+
--score "severity=minor,degraded,blocked" \
|
|
325
|
+
--noul "refund=Does the customer ask for money back?"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
```text
|
|
329
|
+
NAME TYPE ANSWER PROBABILITIES
|
|
330
|
+
team choice billing billing=0.93 engineering=0.02 sales=0.05
|
|
331
|
+
severity score degraded 1.09 (degraded)
|
|
332
|
+
refund noul true 0.87
|
|
333
|
+
backend=laya_mlx route=laya_mlx:ok latency=36.9ms
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`--json` prints a machine-readable payload instead of the table.
|
|
337
|
+
`--min-confidence` and `--model` are also available on `ask`.
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
decide backends
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
```text
|
|
344
|
+
typesafe installed=yes configured=no
|
|
345
|
+
openrouter installed=yes configured=no
|
|
346
|
+
laya installed=yes configured=yes
|
|
347
|
+
laya_mlx installed=yes configured=yes
|
|
348
|
+
crossencoder installed=yes configured=n/a
|
|
349
|
+
llm installed=yes configured=no
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`decide serve --backends a,b --host 127.0.0.1 --port 8811 [--api-key TOKEN] [--min-confidence FLOAT]`
|
|
353
|
+
runs the HTTP server described above.
|
|
354
|
+
|
|
355
|
+
## What the probabilities mean
|
|
356
|
+
|
|
357
|
+
Every probability in a `ChoiceAnswer`, `ScoreAnswer` or `NoulAnswer` is
|
|
358
|
+
whatever the backend reported; `decide` does not calibrate, smooth or
|
|
359
|
+
verify it. What that means differs by backend: `typesafe`, `openrouter` and
|
|
360
|
+
`laya`/`laya_mlx` are purpose-built decision models, but their outputs are
|
|
361
|
+
still self-reported by the model and not audited by this library. The
|
|
362
|
+
`crossencoder` backend turns a relevance reranker's raw logits into a
|
|
363
|
+
softmax or sigmoid - the result is a *normalized score* forced to distribute
|
|
364
|
+
mass over the supplied candidates, not a calibrated probability; a `Choice`
|
|
365
|
+
will still pick a winner even when every candidate is a bad fit, and a
|
|
366
|
+
`Noul` of 0.9 does not mean the condition holds 90% of the time. The `llm`
|
|
367
|
+
backend is the least trustworthy of all: it prompts a general chat model to
|
|
368
|
+
estimate its own confidence in JSON, with no guarantee the model attends to
|
|
369
|
+
every candidate or keeps its numbers well calibrated. Treat all of this
|
|
370
|
+
accordingly - as a signal to gate and fall back on, not as ground truth.
|
|
371
|
+
|
|
372
|
+
## Status
|
|
373
|
+
|
|
374
|
+
`pydecide` is at 0.1.0. The public API may still change before a 1.0
|
|
375
|
+
release.
|
|
376
|
+
|
|
377
|
+
## License
|
|
378
|
+
|
|
379
|
+
MIT, see [LICENSE](LICENSE).
|