semantic-operators 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.
- semantic_operators-0.1.0/.github/workflows/publish.yml +21 -0
- semantic_operators-0.1.0/.gitignore +5 -0
- semantic_operators-0.1.0/LICENSE +21 -0
- semantic_operators-0.1.0/PKG-INFO +195 -0
- semantic_operators-0.1.0/README.md +169 -0
- semantic_operators-0.1.0/benchmarks/run.py +38 -0
- semantic_operators-0.1.0/benchmarks/run_async.py +47 -0
- semantic_operators-0.1.0/benchmarks/support_tickets.py +111 -0
- semantic_operators-0.1.0/examples/compare.py +51 -0
- semantic_operators-0.1.0/examples/hello.py +35 -0
- semantic_operators-0.1.0/pyproject.toml +34 -0
- semantic_operators-0.1.0/src/semantic_operators/__init__.py +6 -0
- semantic_operators-0.1.0/src/semantic_operators/bench.py +172 -0
- semantic_operators-0.1.0/src/semantic_operators/provider.py +22 -0
- semantic_operators-0.1.0/src/semantic_operators/providers/__init__.py +0 -0
- semantic_operators-0.1.0/src/semantic_operators/providers/jev.py +67 -0
- semantic_operators-0.1.0/src/semantic_operators/providers/laya.py +69 -0
- semantic_operators-0.1.0/src/semantic_operators/py.typed +0 -0
- semantic_operators-0.1.0/src/semantic_operators/types.py +57 -0
- semantic_operators-0.1.0/uv.lock +1347 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Publishes to PyPI when a GitHub Release is published.
|
|
2
|
+
# Uses PyPI Trusted Publishing: no API token is stored anywhere.
|
|
3
|
+
# One-time setup on pypi.org: add a (pending) publisher for this repo,
|
|
4
|
+
# workflow "publish.yml", environment "pypi".
|
|
5
|
+
name: Publish to PyPI
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
environment: pypi
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write # lets PyPI verify this workflow's identity
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: astral-sh/setup-uv@v6
|
|
20
|
+
- run: uv build
|
|
21
|
+
- run: uv publish
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jason Duncan
|
|
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.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: semantic-operators
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One small, provider-neutral interface for System One models (Jev, Laya, and compatibles).
|
|
5
|
+
Project-URL: Homepage, https://github.com/jasonduncan/semantic-operators
|
|
6
|
+
Project-URL: Source, https://github.com/jasonduncan/semantic-operators
|
|
7
|
+
Project-URL: Issues, https://github.com/jasonduncan/semantic-operators/issues
|
|
8
|
+
Author: Jason Duncan
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: benchmark,classification,jev,laya,llm,system-one,typesafe
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
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
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Provides-Extra: jev
|
|
22
|
+
Requires-Dist: typesafe-sdk>=0.7.1; extra == 'jev'
|
|
23
|
+
Provides-Extra: laya
|
|
24
|
+
Requires-Dist: laya>=0.3.20; extra == 'laya'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Semantic Operators
|
|
28
|
+
|
|
29
|
+
One small interface for **System One models**: fast models that answer structured
|
|
30
|
+
questions about text or data with probabilities, not prose. [Jev](https://typesafe.ai)
|
|
31
|
+
was the first; [Laya](https://huggingface.co/convaiinnovations/laya) is an open-weight,
|
|
32
|
+
Jev-compatible alternative you can run locally. More are coming. This library lets you
|
|
33
|
+
write your code once and swap the model underneath.
|
|
34
|
+
|
|
35
|
+
| Provider | Where it runs | Install |
|
|
36
|
+
|----------|---------------|---------|
|
|
37
|
+
| `providers.jev.Jev` | TypeSafe's hosted API (needs `TYPESAFE_API_KEY`) | `[jev]` |
|
|
38
|
+
| `providers.laya.Laya` | on your machine (~800 MB download on first use) | `[laya]` |
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pip install "semantic-operators[jev]" # Jev (hosted)
|
|
44
|
+
pip install "semantic-operators[laya]" # Laya (local; pulls in torch)
|
|
45
|
+
pip install "semantic-operators[jev,laya]" # both
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The core alone (`pip install semantic-operators`) has no dependencies.
|
|
49
|
+
|
|
50
|
+
## The whole idea
|
|
51
|
+
|
|
52
|
+
A System One model is asked **named questions about a piece of state** and returns an
|
|
53
|
+
answer with probabilities for each. There are three kinds of question:
|
|
54
|
+
|
|
55
|
+
| Question | You give it | `answer.value` |
|
|
56
|
+
|-----------|-----------------------------------------------|--------------------------------------|
|
|
57
|
+
| `Boolean` | instructions (+ optional true/false meanings) | `True` / `False` |
|
|
58
|
+
| `Choice` | instructions + named options | the chosen option name |
|
|
59
|
+
| `Score` | instructions + ordered rubric levels | expected level as a float, e.g. `1.7` |
|
|
60
|
+
|
|
61
|
+
Every `Answer` also carries `probabilities` (a dict, in the question's option/level
|
|
62
|
+
order) and `raw` (the provider's own answer object).
|
|
63
|
+
|
|
64
|
+
A **provider** is anything with one method:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
def ask(self, state, questions: dict[str, Question]) -> dict[str, Answer]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
That's the entire abstraction.
|
|
71
|
+
|
|
72
|
+
## Quick start
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
echo "TYPESAFE_API_KEY=..." > .env
|
|
76
|
+
uv run --env-file .env --extra jev python examples/hello.py
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from typesafe_sdk import TypeSafeClient
|
|
81
|
+
from semantic_operators import Boolean, Choice, Score
|
|
82
|
+
from semantic_operators.providers.jev import Jev
|
|
83
|
+
|
|
84
|
+
with TypeSafeClient() as client: # you create and own the SDK client
|
|
85
|
+
jev = Jev(client) # model defaults to "jev-latest"
|
|
86
|
+
answers = jev.ask(
|
|
87
|
+
"I was charged twice and I'm furious.",
|
|
88
|
+
{
|
|
89
|
+
"is_complaint": Boolean("Is the customer complaining?"),
|
|
90
|
+
"department": Choice("Which team should handle this?",
|
|
91
|
+
{"billing": "Payments, refunds", "other": "Anything else"}),
|
|
92
|
+
"urgency": Score("How urgent is this?", ["low", "medium", "high"]),
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
answers["department"].value # "billing"
|
|
97
|
+
answers["department"].probabilities # {"billing": 0.97, "other": 0.03}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Swapping to Laya changes only how the provider is built:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import laya
|
|
104
|
+
from semantic_operators.providers.laya import Laya
|
|
105
|
+
|
|
106
|
+
provider = Laya(laya.load("convaiinnovations/laya")) # or Laya(laya.Router())
|
|
107
|
+
answers = provider.ask(state, questions) # same questions, same Answer type
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Compare both side by side:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
uv run --env-file .env --extra jev --extra laya python examples/compare.py
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Benchmark
|
|
117
|
+
|
|
118
|
+
`bench.run(provider, questions, cases)` asks each labeled case all questions in one call
|
|
119
|
+
and reports, per question, **accuracy** (Score values are rounded to the nearest level)
|
|
120
|
+
and **p(correct)**, the average probability the provider gave the right answer, plus
|
|
121
|
+
latency and every miss.
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
uv run --env-file .env --extra jev --extra laya python benchmarks/run.py
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`benchmarks/support_tickets.py` holds 20 hand-written, hand-labeled support messages
|
|
128
|
+
and the same 3 questions in three wordings. `bench.stability(reports)` reports how often
|
|
129
|
+
a provider's decision stays the same when only the wording changes (labels play no part). It's a smoke test, not a verdict: small, authored, one person's labels.
|
|
130
|
+
|
|
131
|
+
## Async
|
|
132
|
+
|
|
133
|
+
Every provider has an async twin with the same contract, `await provider.ask(...)`:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from typesafe_sdk import AsyncTypeSafeClient
|
|
137
|
+
from semantic_operators.providers.jev import AsyncJev
|
|
138
|
+
from semantic_operators.providers.laya import AsyncLaya
|
|
139
|
+
|
|
140
|
+
async with AsyncTypeSafeClient() as client:
|
|
141
|
+
answers = await AsyncJev(client).ask(state, questions)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`AsyncLaya` runs the local model in a worker thread, one call at a time. Concurrency
|
|
145
|
+
speeds up a hosted API (many requests in flight), not a single local model.
|
|
146
|
+
`bench.run_async(provider, questions, cases, concurrency=8)` benchmarks async providers:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
uv run --env-file .env --extra jev --extra laya python benchmarks/run_async.py
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Layout
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
src/semantic_operators/
|
|
156
|
+
types.py Boolean, Choice, Score, Answer: our vocabulary
|
|
157
|
+
provider.py Provider and AsyncProvider (one method each)
|
|
158
|
+
providers/jev.py translates to/from the TypeSafe SDK
|
|
159
|
+
providers/laya.py translates to/from the laya package
|
|
160
|
+
bench.py (higher layer) run labeled cases through a provider, score them
|
|
161
|
+
examples/
|
|
162
|
+
hello.py one real call to Jev
|
|
163
|
+
compare.py the same questions through Jev and Laya
|
|
164
|
+
benchmarks/
|
|
165
|
+
support_tickets.py 20 labeled messages + the questions
|
|
166
|
+
run.py runs the suite through Jev and Laya
|
|
167
|
+
run_async.py concurrency, and both providers at once
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Layers
|
|
171
|
+
|
|
172
|
+
Semantic Operators is built in layers inside one package:
|
|
173
|
+
|
|
174
|
+
1. **Base layer:** a clean, provider-neutral abstraction over System One
|
|
175
|
+
models: `types.py`, `provider.py`, `providers/`.
|
|
176
|
+
2. **Higher layers:** built only on the base layer. So far: `bench.py`. Later: reusable
|
|
177
|
+
named operators and composition.
|
|
178
|
+
|
|
179
|
+
The base layer never imports from a higher layer, so it could later be split out as its
|
|
180
|
+
own package without changing how it's used.
|
|
181
|
+
|
|
182
|
+
## Rules
|
|
183
|
+
|
|
184
|
+
- The library never reads API keys or environment variables. You build the client.
|
|
185
|
+
- The core has no dependencies. Each provider's SDK is an optional extra (`[jev]`, `[laya]`).
|
|
186
|
+
- Our names, not the provider's: `Boolean`, not `noul`.
|
|
187
|
+
|
|
188
|
+
## Not here yet (on purpose)
|
|
189
|
+
|
|
190
|
+
Reusable named operators, error types, and
|
|
191
|
+
"don't know" answers. Each will be added as its own small step.
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Semantic Operators
|
|
2
|
+
|
|
3
|
+
One small interface for **System One models**: fast models that answer structured
|
|
4
|
+
questions about text or data with probabilities, not prose. [Jev](https://typesafe.ai)
|
|
5
|
+
was the first; [Laya](https://huggingface.co/convaiinnovations/laya) is an open-weight,
|
|
6
|
+
Jev-compatible alternative you can run locally. More are coming. This library lets you
|
|
7
|
+
write your code once and swap the model underneath.
|
|
8
|
+
|
|
9
|
+
| Provider | Where it runs | Install |
|
|
10
|
+
|----------|---------------|---------|
|
|
11
|
+
| `providers.jev.Jev` | TypeSafe's hosted API (needs `TYPESAFE_API_KEY`) | `[jev]` |
|
|
12
|
+
| `providers.laya.Laya` | on your machine (~800 MB download on first use) | `[laya]` |
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
pip install "semantic-operators[jev]" # Jev (hosted)
|
|
18
|
+
pip install "semantic-operators[laya]" # Laya (local; pulls in torch)
|
|
19
|
+
pip install "semantic-operators[jev,laya]" # both
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The core alone (`pip install semantic-operators`) has no dependencies.
|
|
23
|
+
|
|
24
|
+
## The whole idea
|
|
25
|
+
|
|
26
|
+
A System One model is asked **named questions about a piece of state** and returns an
|
|
27
|
+
answer with probabilities for each. There are three kinds of question:
|
|
28
|
+
|
|
29
|
+
| Question | You give it | `answer.value` |
|
|
30
|
+
|-----------|-----------------------------------------------|--------------------------------------|
|
|
31
|
+
| `Boolean` | instructions (+ optional true/false meanings) | `True` / `False` |
|
|
32
|
+
| `Choice` | instructions + named options | the chosen option name |
|
|
33
|
+
| `Score` | instructions + ordered rubric levels | expected level as a float, e.g. `1.7` |
|
|
34
|
+
|
|
35
|
+
Every `Answer` also carries `probabilities` (a dict, in the question's option/level
|
|
36
|
+
order) and `raw` (the provider's own answer object).
|
|
37
|
+
|
|
38
|
+
A **provider** is anything with one method:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
def ask(self, state, questions: dict[str, Question]) -> dict[str, Answer]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That's the entire abstraction.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
echo "TYPESAFE_API_KEY=..." > .env
|
|
50
|
+
uv run --env-file .env --extra jev python examples/hello.py
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from typesafe_sdk import TypeSafeClient
|
|
55
|
+
from semantic_operators import Boolean, Choice, Score
|
|
56
|
+
from semantic_operators.providers.jev import Jev
|
|
57
|
+
|
|
58
|
+
with TypeSafeClient() as client: # you create and own the SDK client
|
|
59
|
+
jev = Jev(client) # model defaults to "jev-latest"
|
|
60
|
+
answers = jev.ask(
|
|
61
|
+
"I was charged twice and I'm furious.",
|
|
62
|
+
{
|
|
63
|
+
"is_complaint": Boolean("Is the customer complaining?"),
|
|
64
|
+
"department": Choice("Which team should handle this?",
|
|
65
|
+
{"billing": "Payments, refunds", "other": "Anything else"}),
|
|
66
|
+
"urgency": Score("How urgent is this?", ["low", "medium", "high"]),
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
answers["department"].value # "billing"
|
|
71
|
+
answers["department"].probabilities # {"billing": 0.97, "other": 0.03}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Swapping to Laya changes only how the provider is built:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import laya
|
|
78
|
+
from semantic_operators.providers.laya import Laya
|
|
79
|
+
|
|
80
|
+
provider = Laya(laya.load("convaiinnovations/laya")) # or Laya(laya.Router())
|
|
81
|
+
answers = provider.ask(state, questions) # same questions, same Answer type
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Compare both side by side:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
uv run --env-file .env --extra jev --extra laya python examples/compare.py
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Benchmark
|
|
91
|
+
|
|
92
|
+
`bench.run(provider, questions, cases)` asks each labeled case all questions in one call
|
|
93
|
+
and reports, per question, **accuracy** (Score values are rounded to the nearest level)
|
|
94
|
+
and **p(correct)**, the average probability the provider gave the right answer, plus
|
|
95
|
+
latency and every miss.
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
uv run --env-file .env --extra jev --extra laya python benchmarks/run.py
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`benchmarks/support_tickets.py` holds 20 hand-written, hand-labeled support messages
|
|
102
|
+
and the same 3 questions in three wordings. `bench.stability(reports)` reports how often
|
|
103
|
+
a provider's decision stays the same when only the wording changes (labels play no part). It's a smoke test, not a verdict: small, authored, one person's labels.
|
|
104
|
+
|
|
105
|
+
## Async
|
|
106
|
+
|
|
107
|
+
Every provider has an async twin with the same contract, `await provider.ask(...)`:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from typesafe_sdk import AsyncTypeSafeClient
|
|
111
|
+
from semantic_operators.providers.jev import AsyncJev
|
|
112
|
+
from semantic_operators.providers.laya import AsyncLaya
|
|
113
|
+
|
|
114
|
+
async with AsyncTypeSafeClient() as client:
|
|
115
|
+
answers = await AsyncJev(client).ask(state, questions)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`AsyncLaya` runs the local model in a worker thread, one call at a time. Concurrency
|
|
119
|
+
speeds up a hosted API (many requests in flight), not a single local model.
|
|
120
|
+
`bench.run_async(provider, questions, cases, concurrency=8)` benchmarks async providers:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
uv run --env-file .env --extra jev --extra laya python benchmarks/run_async.py
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Layout
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
src/semantic_operators/
|
|
130
|
+
types.py Boolean, Choice, Score, Answer: our vocabulary
|
|
131
|
+
provider.py Provider and AsyncProvider (one method each)
|
|
132
|
+
providers/jev.py translates to/from the TypeSafe SDK
|
|
133
|
+
providers/laya.py translates to/from the laya package
|
|
134
|
+
bench.py (higher layer) run labeled cases through a provider, score them
|
|
135
|
+
examples/
|
|
136
|
+
hello.py one real call to Jev
|
|
137
|
+
compare.py the same questions through Jev and Laya
|
|
138
|
+
benchmarks/
|
|
139
|
+
support_tickets.py 20 labeled messages + the questions
|
|
140
|
+
run.py runs the suite through Jev and Laya
|
|
141
|
+
run_async.py concurrency, and both providers at once
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Layers
|
|
145
|
+
|
|
146
|
+
Semantic Operators is built in layers inside one package:
|
|
147
|
+
|
|
148
|
+
1. **Base layer:** a clean, provider-neutral abstraction over System One
|
|
149
|
+
models: `types.py`, `provider.py`, `providers/`.
|
|
150
|
+
2. **Higher layers:** built only on the base layer. So far: `bench.py`. Later: reusable
|
|
151
|
+
named operators and composition.
|
|
152
|
+
|
|
153
|
+
The base layer never imports from a higher layer, so it could later be split out as its
|
|
154
|
+
own package without changing how it's used.
|
|
155
|
+
|
|
156
|
+
## Rules
|
|
157
|
+
|
|
158
|
+
- The library never reads API keys or environment variables. You build the client.
|
|
159
|
+
- The core has no dependencies. Each provider's SDK is an optional extra (`[jev]`, `[laya]`).
|
|
160
|
+
- Our names, not the provider's: `Boolean`, not `noul`.
|
|
161
|
+
|
|
162
|
+
## Not here yet (on purpose)
|
|
163
|
+
|
|
164
|
+
Reusable named operators, error types, and
|
|
165
|
+
"don't know" answers. Each will be added as its own small step.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Run the support-ticket benchmark through Jev and Laya, in every wording.
|
|
2
|
+
|
|
3
|
+
Run: uv run --env-file .env --extra jev --extra laya python benchmarks/run.py
|
|
4
|
+
Makes 63 Jev API calls (3 wordings x (1 warm-up + 20 cases)).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import statistics
|
|
8
|
+
|
|
9
|
+
import laya
|
|
10
|
+
from typesafe_sdk import TypeSafeClient
|
|
11
|
+
|
|
12
|
+
from semantic_operators import Provider
|
|
13
|
+
from semantic_operators.bench import run, stability
|
|
14
|
+
from semantic_operators.providers.jev import Jev
|
|
15
|
+
from semantic_operators.providers.laya import Laya
|
|
16
|
+
from support_tickets import cases, wordings
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def benchmark(name: str, provider: Provider) -> None:
|
|
20
|
+
reports = {wording: run(provider, qs, cases) for wording, qs in wordings.items()}
|
|
21
|
+
stable = stability(list(reports.values()))
|
|
22
|
+
latencies = [ms for r in reports.values() for ms in r.latencies_ms]
|
|
23
|
+
|
|
24
|
+
print(f"\n{name}: median {statistics.median(latencies):.0f} ms per call")
|
|
25
|
+
print(" accuracy, p(correct) in brackets")
|
|
26
|
+
print(f" {'question':<14}" + "".join(f"{w:>18}" for w in reports) + f"{'stability':>12}")
|
|
27
|
+
for question in stable:
|
|
28
|
+
cells = "".join(
|
|
29
|
+
f"{f'{s.correct}/{s.total} ({s.mean_p_correct:.2f})':>18}"
|
|
30
|
+
for s in (r.questions[question] for r in reports.values())
|
|
31
|
+
)
|
|
32
|
+
print(f" {question:<14}{cells}{stable[question]:>12.0%}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
with TypeSafeClient() as client:
|
|
36
|
+
benchmark("Jev", Jev(client))
|
|
37
|
+
|
|
38
|
+
benchmark("Laya", Laya(laya.load("convaiinnovations/laya")))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""What concurrency buys each provider, and both providers benchmarked at once.
|
|
2
|
+
|
|
3
|
+
Run: uv run --env-file .env --extra jev --extra laya python benchmarks/run_async.py
|
|
4
|
+
Makes 63 Jev API calls (3 runs x (1 warm-up + 20 cases)).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import statistics
|
|
9
|
+
|
|
10
|
+
import laya
|
|
11
|
+
from typesafe_sdk import AsyncTypeSafeClient
|
|
12
|
+
|
|
13
|
+
from semantic_operators.bench import Report, run_async
|
|
14
|
+
from semantic_operators.providers.jev import AsyncJev
|
|
15
|
+
from semantic_operators.providers.laya import AsyncLaya
|
|
16
|
+
from support_tickets import cases, questions
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def show(label: str, report: Report) -> None:
|
|
20
|
+
correct = sum(s.correct for s in report.questions.values())
|
|
21
|
+
total = sum(s.total for s in report.questions.values())
|
|
22
|
+
print(f" {label:<28} total {report.total_ms:>6.0f} ms "
|
|
23
|
+
f"per call: median {statistics.median(report.latencies_ms):>4.0f} ms, "
|
|
24
|
+
f"slowest {max(report.latencies_ms):>4.0f} ms correct {correct}/{total}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def main() -> None:
|
|
28
|
+
async with AsyncTypeSafeClient() as client:
|
|
29
|
+
jev = AsyncJev(client)
|
|
30
|
+
lay = AsyncLaya(laya.load("convaiinnovations/laya"))
|
|
31
|
+
|
|
32
|
+
for name, provider in [("Jev", jev), ("Laya", lay)]:
|
|
33
|
+
print(f"\n{name}")
|
|
34
|
+
for concurrency in (1, 8):
|
|
35
|
+
report = await run_async(provider, questions, cases, concurrency=concurrency)
|
|
36
|
+
show(f"concurrency {concurrency}", report)
|
|
37
|
+
|
|
38
|
+
print("\nBoth at once (concurrency 8 each)")
|
|
39
|
+
jev_report, laya_report = await asyncio.gather(
|
|
40
|
+
run_async(jev, questions, cases, concurrency=8),
|
|
41
|
+
run_async(lay, questions, cases, concurrency=8),
|
|
42
|
+
)
|
|
43
|
+
show("Jev", jev_report)
|
|
44
|
+
show("Laya", laya_report)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Support-ticket triage: 20 hand-written messages, labeled by hand.
|
|
2
|
+
|
|
3
|
+
These are authored examples, not real customer data, and one person's labels.
|
|
4
|
+
"Urgency" in particular is a judgment call. Treat results as a smoke test of
|
|
5
|
+
the providers, not a verdict on them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from semantic_operators import Boolean, Choice, Score
|
|
9
|
+
from semantic_operators.bench import Case
|
|
10
|
+
|
|
11
|
+
# The same three questions, worded three ways. Option names and level order are
|
|
12
|
+
# identical across wordings, so one set of labels applies to all of them.
|
|
13
|
+
wordings = {
|
|
14
|
+
"descriptive": {
|
|
15
|
+
"is_complaint": Boolean("Is the customer complaining or expressing dissatisfaction?"),
|
|
16
|
+
"department": Choice(
|
|
17
|
+
"Which team should handle this message?",
|
|
18
|
+
{
|
|
19
|
+
"billing": "Charges, invoices, payments, refunds, or pricing.",
|
|
20
|
+
"technical": "Bugs, errors, outages, or difficulty using the product.",
|
|
21
|
+
"account": "Logging in, passwords, profile details, or account access and closure.",
|
|
22
|
+
"other": "Anything else, such as feedback, partnerships, or general questions.",
|
|
23
|
+
},
|
|
24
|
+
),
|
|
25
|
+
"urgency": Score(
|
|
26
|
+
"How urgently does this need a response?",
|
|
27
|
+
["low: can wait days", "medium: should be handled today", "high: needs attention now"],
|
|
28
|
+
),
|
|
29
|
+
},
|
|
30
|
+
"plain": {
|
|
31
|
+
"is_complaint": Boolean("Is this a complaint?"),
|
|
32
|
+
"department": Choice(
|
|
33
|
+
"Which department?",
|
|
34
|
+
{"billing": None, "technical": None, "account": None, "other": None},
|
|
35
|
+
),
|
|
36
|
+
"urgency": Score("How urgent is this?", ["not urgent", "soon", "urgent"]),
|
|
37
|
+
},
|
|
38
|
+
"reworded": {
|
|
39
|
+
"is_complaint": Boolean("Does the writer express frustration, disappointment, or a grievance?"),
|
|
40
|
+
"department": Choice(
|
|
41
|
+
"Route this message to the right team.",
|
|
42
|
+
{
|
|
43
|
+
"billing": "Money: payments, invoices, refunds, prices.",
|
|
44
|
+
"technical": "The product not working as expected.",
|
|
45
|
+
"account": "Sign-in, credentials, account settings, or deleting the account.",
|
|
46
|
+
"other": "None of the above.",
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
"urgency": Score(
|
|
50
|
+
"How quickly should support reply?",
|
|
51
|
+
["whenever convenient", "within the day", "immediately"],
|
|
52
|
+
),
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
questions = wordings["descriptive"]
|
|
57
|
+
|
|
58
|
+
LOW, MEDIUM, HIGH = 0, 1, 2 # urgency labels are level indexes
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def case(state: str, complaint: bool, department: str, urgency: int) -> Case:
|
|
62
|
+
return Case(state, {"is_complaint": complaint, "department": department, "urgency": urgency})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
cases = [
|
|
66
|
+
# Clear-cut
|
|
67
|
+
case("I was charged twice for my subscription this month. Please refund one of them.",
|
|
68
|
+
True, "billing", MEDIUM),
|
|
69
|
+
case("Your whole site has been down for an hour and our store can't take any orders!",
|
|
70
|
+
True, "technical", HIGH),
|
|
71
|
+
case("How do I change the email address on my profile?",
|
|
72
|
+
False, "account", LOW),
|
|
73
|
+
case("Can you send me a copy of last month's invoice for our accountant?",
|
|
74
|
+
False, "billing", LOW),
|
|
75
|
+
case("The export button does nothing when I click it. Tried Chrome and Safari.",
|
|
76
|
+
True, "technical", MEDIUM),
|
|
77
|
+
case("I think someone else logged into my account, I see orders I never placed. Help!",
|
|
78
|
+
True, "account", HIGH),
|
|
79
|
+
case("Just wanted to say the new dashboard is fantastic. Great work, team!",
|
|
80
|
+
False, "other", LOW),
|
|
81
|
+
case("We're a design agency interested in a partnership. Who should we talk to?",
|
|
82
|
+
False, "other", LOW),
|
|
83
|
+
case("Payment failed three times at checkout and now my card is locked by the bank.",
|
|
84
|
+
True, "billing", HIGH),
|
|
85
|
+
case("I forgot my password and the reset email never arrives.",
|
|
86
|
+
True, "account", MEDIUM),
|
|
87
|
+
|
|
88
|
+
# Harder: sarcasm, politeness masking a problem, mixed topics, JSON state
|
|
89
|
+
case("Oh great, another 'minor update' that wiped all my saved reports. Love it.",
|
|
90
|
+
True, "technical", HIGH),
|
|
91
|
+
case("No rush at all, but I noticed the annual plan price on your site doesn't match "
|
|
92
|
+
"what I was billed.",
|
|
93
|
+
True, "billing", LOW),
|
|
94
|
+
case("Please delete my account and all my data. I no longer want to use this service.",
|
|
95
|
+
False, "account", MEDIUM),
|
|
96
|
+
case("Is there a discount for nonprofits?",
|
|
97
|
+
False, "billing", LOW),
|
|
98
|
+
case("The app keeps crashing, and since I can't use it I want a refund for this month.",
|
|
99
|
+
True, "billing", MEDIUM),
|
|
100
|
+
case("Our CEO is presenting from your platform in 20 minutes and nothing loads.",
|
|
101
|
+
True, "technical", HIGH),
|
|
102
|
+
case("Thanks for fixing the login bug so quickly yesterday!",
|
|
103
|
+
False, "other", LOW),
|
|
104
|
+
case("I've emailed three times about my refund and nobody has replied. Unacceptable.",
|
|
105
|
+
True, "billing", HIGH),
|
|
106
|
+
case({"channel": "chat", "plan": "enterprise", "message": "SSO login is failing for all "
|
|
107
|
+
"500 of our users since this morning."},
|
|
108
|
+
True, "account", HIGH),
|
|
109
|
+
case({"channel": "email", "plan": "free", "message": "Where can I find your API docs?"},
|
|
110
|
+
False, "other", LOW),
|
|
111
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Ask Jev (hosted) and Laya (local) the same questions, through the same interface.
|
|
2
|
+
|
|
3
|
+
Run: uv run --env-file .env --extra jev --extra laya python examples/compare.py
|
|
4
|
+
The first run downloads the Laya checkpoint (~800 MB) from Hugging Face.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import laya
|
|
10
|
+
from typesafe_sdk import TypeSafeClient
|
|
11
|
+
|
|
12
|
+
from semantic_operators import Boolean, Choice, Provider, Score
|
|
13
|
+
from semantic_operators.providers.jev import Jev
|
|
14
|
+
from semantic_operators.providers.laya import Laya
|
|
15
|
+
|
|
16
|
+
message = "I was charged twice for my subscription this month and I'm furious."
|
|
17
|
+
|
|
18
|
+
questions = {
|
|
19
|
+
"is_complaint": Boolean("Is the customer complaining?"),
|
|
20
|
+
"department": Choice(
|
|
21
|
+
"Which team should handle this?",
|
|
22
|
+
{
|
|
23
|
+
"billing": "Charges, invoices, payments, or refunds.",
|
|
24
|
+
"technical": "A product defect or difficulty using the product.",
|
|
25
|
+
"other": "Anything else.",
|
|
26
|
+
},
|
|
27
|
+
),
|
|
28
|
+
"urgency": Score("How urgent is this request?", ["low", "medium", "high"]),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def show(name: str, provider: Provider) -> None:
|
|
33
|
+
start = time.perf_counter()
|
|
34
|
+
answers = provider.ask(message, questions)
|
|
35
|
+
ms = (time.perf_counter() - start) * 1000
|
|
36
|
+
print(f"\n{name} ({ms:.0f} ms)")
|
|
37
|
+
for question, answer in answers.items():
|
|
38
|
+
value = f"{answer.value:.2f}" if isinstance(answer.value, float) else repr(answer.value)
|
|
39
|
+
probabilities = ", ".join(f"{k}={p:.2f}" for k, p in answer.probabilities.items())
|
|
40
|
+
print(f" {question:>13}: {value:<12} ({probabilities})")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
print(f"Message: {message}")
|
|
44
|
+
|
|
45
|
+
with TypeSafeClient() as client:
|
|
46
|
+
show("Jev", Jev(client))
|
|
47
|
+
|
|
48
|
+
model = laya.load("convaiinnovations/laya") # English checkpoint, runs on this machine
|
|
49
|
+
laya_provider = Laya(model)
|
|
50
|
+
laya_provider.ask(message, questions) # warm-up: the first call pays one-time setup costs
|
|
51
|
+
show("Laya", laya_provider)
|