needlepath 0.2.0__tar.gz → 0.2.2__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.
- {needlepath-0.2.0 → needlepath-0.2.2}/CHANGELOG.md +33 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/PKG-INFO +87 -7
- {needlepath-0.2.0 → needlepath-0.2.2}/README.md +85 -5
- {needlepath-0.2.0 → needlepath-0.2.2}/pyproject.toml +2 -1
- {needlepath-0.2.0 → needlepath-0.2.2}/src/needlepath/__init__.py +17 -1
- {needlepath-0.2.0 → needlepath-0.2.2}/src/needlepath/client.py +233 -15
- needlepath-0.2.2/src/needlepath/integrations/__init__.py +30 -0
- needlepath-0.2.2/src/needlepath/integrations/system_one.py +210 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/src/needlepath/models.py +210 -4
- {needlepath-0.2.0 → needlepath-0.2.2}/src/needlepath/result.py +24 -1
- {needlepath-0.2.0 → needlepath-0.2.2}/src/needlepath/version.py +1 -1
- {needlepath-0.2.0 → needlepath-0.2.2}/tests/test_async_parity.py +3 -1
- {needlepath-0.2.0 → needlepath-0.2.2}/tests/test_fail_open.py +158 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/tests/test_live_endpoint.py +7 -5
- needlepath-0.2.2/tests/test_r4_contract.py +195 -0
- needlepath-0.2.2/tests/test_system_one_adapter.py +337 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/tests/test_wire_compatibility.py +91 -4
- {needlepath-0.2.0 → needlepath-0.2.2}/.gitignore +0 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/LICENSE +0 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/src/needlepath/errors.py +0 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/src/needlepath/features.py +0 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/src/needlepath/py.typed +0 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/src/needlepath/testing.py +0 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/src/needlepath/transport.py +0 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/tests/test_config_and_guards.py +0 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/tests/test_no_secret_leak.py +0 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/tests/test_retries.py +0 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/tests/test_shadow_mode.py +0 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/tests/test_simple_shape.py +0 -0
- {needlepath-0.2.0 → needlepath-0.2.2}/tests/test_urllib_transport.py +0 -0
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 0.2.2
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- First published build carrying the `sanitized_fallback` and `blocked`
|
|
6
|
+
security-screening outcomes, `security_mode` on requests and the
|
|
7
|
+
`security_action` receipt field (see the API release notes). 0.2.1 predates
|
|
8
|
+
them and classifies a screened fallback as `selection_unsafe`.
|
|
9
|
+
|
|
1
10
|
# Changelog
|
|
2
11
|
|
|
3
12
|
All notable changes to the `needlepath` Python package are documented here.
|
|
@@ -5,6 +14,30 @@ All notable changes to the `needlepath` Python package are documented here.
|
|
|
5
14
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
15
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
16
|
|
|
17
|
+
## [Unreleased]
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **System One state adapter**: `needlepath.integrations.system_one` reshapes a
|
|
22
|
+
selection into the `{state, questions}` request body that small classifier
|
|
23
|
+
models (TypeSafe's Jev, Laya) accept. `to_system_one_state()` carries every
|
|
24
|
+
record in `response.selected`, in the order returned, with the text
|
|
25
|
+
unmodified; `mode="rendered"` returns `state` as one string instead of an
|
|
26
|
+
array. `questions` travels in the shape it was passed, mapping or sequence,
|
|
27
|
+
so a `{id: question}` mapping never arrives as a list of its keys.
|
|
28
|
+
A `SelectionResult` is accepted only when it `applied`; a passthrough raises
|
|
29
|
+
`NeedlepathConfigError` naming the reason, since its response can carry a
|
|
30
|
+
selection the client declined to use. `selection_receipt()` returns
|
|
31
|
+
`request_id`, `selection_id`, `policy_version`, `tokens_saved` and `outcome`
|
|
32
|
+
as a sidecar dict, prefixed the way `SelectionResult.metadata()` prefixes its
|
|
33
|
+
keys.
|
|
34
|
+
|
|
35
|
+
## [0.2.1] - 2026-09-03
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Docs, docstrings and examples pin np-2026-08-r4. No behaviour change.
|
|
40
|
+
|
|
8
41
|
## [0.2.0] - 2026-08-07
|
|
9
42
|
|
|
10
43
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: needlepath
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Framework-free client for the Needlepath hosted context-selection service.
|
|
5
5
|
Project-URL: Homepage, https://nextmoca.com
|
|
6
6
|
Author: Next Moca Global, Inc.
|
|
@@ -57,7 +57,7 @@ from needlepath import NeedlepathClient, ContextRecord, TaskSpec
|
|
|
57
57
|
|
|
58
58
|
client = NeedlepathClient(
|
|
59
59
|
api_key="np_live_...", # or NEEDLEPATH_API_KEY
|
|
60
|
-
operating_point="np-2026-
|
|
60
|
+
operating_point="np-2026-08-r4", # required — see "Pin the operating point"
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
result = client.select(
|
|
@@ -80,7 +80,7 @@ original context. That is the whole integration contract.
|
|
|
80
80
|
```python
|
|
81
81
|
from needlepath import AsyncNeedlepathClient
|
|
82
82
|
|
|
83
|
-
async with AsyncNeedlepathClient(operating_point="np-2026-
|
|
83
|
+
async with AsyncNeedlepathClient(operating_point="np-2026-08-r4") as client:
|
|
84
84
|
result = await client.select(records=..., task=..., max_context_tokens=4000)
|
|
85
85
|
```
|
|
86
86
|
|
|
@@ -88,6 +88,41 @@ Identical semantics — the decision logic is literally the same functions. Uses
|
|
|
88
88
|
`httpx.AsyncClient` when httpx is importable, a worker thread otherwise; it
|
|
89
89
|
never blocks the event loop either way.
|
|
90
90
|
|
|
91
|
+
### R4 trace and outcome attribution
|
|
92
|
+
|
|
93
|
+
Pin `np-2026-08-r4` and opt into the metadata-only trace when you need to
|
|
94
|
+
inspect record dispositions:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
result = client.select(
|
|
98
|
+
records=records,
|
|
99
|
+
task=TaskSpec(prompt=question),
|
|
100
|
+
max_context_tokens=4000,
|
|
101
|
+
return_trace=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if result.response and result.response.selection_trace:
|
|
105
|
+
for item in result.response.selection_trace.records:
|
|
106
|
+
print(item.record_id, item.disposition, item.reason)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The hosted response also supplies `result.selection_id`, a server-issued id
|
|
110
|
+
separate from your request id. Report downstream quality after the model call:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
if result.selection_id:
|
|
114
|
+
client.report_outcome(
|
|
115
|
+
result.selection_id,
|
|
116
|
+
task_success=True,
|
|
117
|
+
context_sufficient=True,
|
|
118
|
+
evaluation_source="application",
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`report_outcome()` is idempotent and independently retryable. It can raise on a
|
|
123
|
+
callback failure; that never changes `select()` or the context already returned.
|
|
124
|
+
Do not send prompts, context, model output, answers, or free-form notes.
|
|
125
|
+
|
|
91
126
|
## Configuration
|
|
92
127
|
|
|
93
128
|
| Argument | Environment | Default |
|
|
@@ -130,6 +165,7 @@ if not result.applied:
|
|
|
130
165
|
| `disabled` | no | the integration was constructed disabled |
|
|
131
166
|
| `no_records` | no | nothing to select over; no call was made |
|
|
132
167
|
| `below_trigger` | no | under the configured trigger; no call was made |
|
|
168
|
+
| `escalated` | no | the engine declined: full context over capacity. **Charged at the full rate**, unlike a free stand-down |
|
|
133
169
|
| `empty_selection` | no | zero records selected |
|
|
134
170
|
| `engine_fallback` | no | `fallback_used` — the engine wants the full context |
|
|
135
171
|
| `selection_error` | no | the response carried a `selection_error` |
|
|
@@ -158,7 +194,7 @@ customer absorbs; failing open on "no operating point was pinned" accepts
|
|
|
158
194
|
exactly the behaviour drift the rule exists to prevent. It fails at wire-up,
|
|
159
195
|
never under load.
|
|
160
196
|
|
|
161
|
-
Labels are immutable: `np-2026-
|
|
197
|
+
Labels are immutable: `np-2026-08-r4` means one frozen configuration forever, and
|
|
162
198
|
a retune mints a new label. `client.operating_points()` lists what the endpoint
|
|
163
199
|
serves — call it once at wire-up if you want the assertion, never per request.
|
|
164
200
|
|
|
@@ -176,7 +212,7 @@ that passes at 5.99 MB can still be rejected upstream. Lower
|
|
|
176
212
|
### 4. Shadow mode is a one-line switch
|
|
177
213
|
|
|
178
214
|
```python
|
|
179
|
-
client = NeedlepathClient(operating_point="np-2026-
|
|
215
|
+
client = NeedlepathClient(operating_point="np-2026-08-r4", shadow=True)
|
|
180
216
|
```
|
|
181
217
|
|
|
182
218
|
The call is made exactly as a live call would be — same body, same billing, same
|
|
@@ -222,13 +258,57 @@ carry fragments of your input in an exception message — the Lambda deliberatel
|
|
|
222
258
|
declines to log it for that reason — so only its *type* appears here. The full
|
|
223
259
|
string stays on `result.response.selection_error`.
|
|
224
260
|
|
|
261
|
+
## System One models (TypeSafe Jev, Laya)
|
|
262
|
+
|
|
263
|
+
Small classifier models that take `{state, questions}` and answer the questions
|
|
264
|
+
against the state. `needlepath.integrations.system_one` reshapes a selection
|
|
265
|
+
into that request body:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from needlepath.integrations.system_one import selection_receipt, to_system_one_state
|
|
269
|
+
|
|
270
|
+
result = client.select(records=records, task=task, max_context_tokens=4000)
|
|
271
|
+
payload = to_system_one_state(
|
|
272
|
+
# Fail open with your own records: a result that did not apply is refused.
|
|
273
|
+
result if result.applied else records,
|
|
274
|
+
questions={"refund": Noul(instructions="Is this a refund request?")},
|
|
275
|
+
)
|
|
276
|
+
# Sidecar metadata, not part of the body.
|
|
277
|
+
receipt = selection_receipt(result)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`payload["state"]` is a JSON array of `{record_id, title, source, text}`, one
|
|
281
|
+
entry per record in `response.selected`, **in the order the service returned
|
|
282
|
+
them, with the text unmodified**. The adapter is transport only: no threshold on
|
|
283
|
+
`score`, no drop for an empty excerpt, no re-sort, no deduplication, and no
|
|
284
|
+
question you did not pass. `mode="rendered"` returns `state` as a single string
|
|
285
|
+
instead, records joined by blank lines under a one-line `[record_id] title`
|
|
286
|
+
header, for a classifier whose state argument is text.
|
|
287
|
+
|
|
288
|
+
`questions` travels in the shape you pass it. `langchain-typesafe` reads a
|
|
289
|
+
mapping of id to typed question (`Noul`, `Choice`, `Score`); a different
|
|
290
|
+
`{state, questions}` service may read a list. Both are copied and sent
|
|
291
|
+
unchanged, so a mapping never arrives as a list of its keys. A bare string is
|
|
292
|
+
refused rather than iterated into single-character questions.
|
|
293
|
+
|
|
294
|
+
`to_system_one_state` accepts a `SelectionResult` only when it `applied`. A
|
|
295
|
+
result that did not apply is a passthrough — your own records are what has to
|
|
296
|
+
be sent — and its `response` can still carry a selection the client itself
|
|
297
|
+
declined to use, so passing one raises `NeedlepathConfigError` naming the
|
|
298
|
+
reason. To reshape that response anyway, for an audit or a shadow report, pass
|
|
299
|
+
`result.response`.
|
|
300
|
+
|
|
301
|
+
`selection_receipt()` returns `request_id`, `selection_id`, `policy_version`,
|
|
302
|
+
`tokens_saved` and `outcome`, prefixed the way `result.metadata()` prefixes its
|
|
303
|
+
keys. Attribution travels beside the request; record text never travels in it.
|
|
304
|
+
|
|
225
305
|
## Testing your integration
|
|
226
306
|
|
|
227
307
|
```python
|
|
228
308
|
from needlepath.testing import FakeTransport, response_payload, ok, error
|
|
229
309
|
|
|
230
310
|
transport = FakeTransport([error(503), ok(response_payload("np-1"))])
|
|
231
|
-
client = NeedlepathClient(operating_point="np-2026-
|
|
311
|
+
client = NeedlepathClient(operating_point="np-2026-08-r4", transport=transport)
|
|
232
312
|
```
|
|
233
313
|
|
|
234
314
|
`needlepath.testing` is public API, not a private helper. Every rule above is a
|
|
@@ -28,7 +28,7 @@ from needlepath import NeedlepathClient, ContextRecord, TaskSpec
|
|
|
28
28
|
|
|
29
29
|
client = NeedlepathClient(
|
|
30
30
|
api_key="np_live_...", # or NEEDLEPATH_API_KEY
|
|
31
|
-
operating_point="np-2026-
|
|
31
|
+
operating_point="np-2026-08-r4", # required — see "Pin the operating point"
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
result = client.select(
|
|
@@ -51,7 +51,7 @@ original context. That is the whole integration contract.
|
|
|
51
51
|
```python
|
|
52
52
|
from needlepath import AsyncNeedlepathClient
|
|
53
53
|
|
|
54
|
-
async with AsyncNeedlepathClient(operating_point="np-2026-
|
|
54
|
+
async with AsyncNeedlepathClient(operating_point="np-2026-08-r4") as client:
|
|
55
55
|
result = await client.select(records=..., task=..., max_context_tokens=4000)
|
|
56
56
|
```
|
|
57
57
|
|
|
@@ -59,6 +59,41 @@ Identical semantics — the decision logic is literally the same functions. Uses
|
|
|
59
59
|
`httpx.AsyncClient` when httpx is importable, a worker thread otherwise; it
|
|
60
60
|
never blocks the event loop either way.
|
|
61
61
|
|
|
62
|
+
### R4 trace and outcome attribution
|
|
63
|
+
|
|
64
|
+
Pin `np-2026-08-r4` and opt into the metadata-only trace when you need to
|
|
65
|
+
inspect record dispositions:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
result = client.select(
|
|
69
|
+
records=records,
|
|
70
|
+
task=TaskSpec(prompt=question),
|
|
71
|
+
max_context_tokens=4000,
|
|
72
|
+
return_trace=True,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if result.response and result.response.selection_trace:
|
|
76
|
+
for item in result.response.selection_trace.records:
|
|
77
|
+
print(item.record_id, item.disposition, item.reason)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The hosted response also supplies `result.selection_id`, a server-issued id
|
|
81
|
+
separate from your request id. Report downstream quality after the model call:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
if result.selection_id:
|
|
85
|
+
client.report_outcome(
|
|
86
|
+
result.selection_id,
|
|
87
|
+
task_success=True,
|
|
88
|
+
context_sufficient=True,
|
|
89
|
+
evaluation_source="application",
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`report_outcome()` is idempotent and independently retryable. It can raise on a
|
|
94
|
+
callback failure; that never changes `select()` or the context already returned.
|
|
95
|
+
Do not send prompts, context, model output, answers, or free-form notes.
|
|
96
|
+
|
|
62
97
|
## Configuration
|
|
63
98
|
|
|
64
99
|
| Argument | Environment | Default |
|
|
@@ -101,6 +136,7 @@ if not result.applied:
|
|
|
101
136
|
| `disabled` | no | the integration was constructed disabled |
|
|
102
137
|
| `no_records` | no | nothing to select over; no call was made |
|
|
103
138
|
| `below_trigger` | no | under the configured trigger; no call was made |
|
|
139
|
+
| `escalated` | no | the engine declined: full context over capacity. **Charged at the full rate**, unlike a free stand-down |
|
|
104
140
|
| `empty_selection` | no | zero records selected |
|
|
105
141
|
| `engine_fallback` | no | `fallback_used` — the engine wants the full context |
|
|
106
142
|
| `selection_error` | no | the response carried a `selection_error` |
|
|
@@ -129,7 +165,7 @@ customer absorbs; failing open on "no operating point was pinned" accepts
|
|
|
129
165
|
exactly the behaviour drift the rule exists to prevent. It fails at wire-up,
|
|
130
166
|
never under load.
|
|
131
167
|
|
|
132
|
-
Labels are immutable: `np-2026-
|
|
168
|
+
Labels are immutable: `np-2026-08-r4` means one frozen configuration forever, and
|
|
133
169
|
a retune mints a new label. `client.operating_points()` lists what the endpoint
|
|
134
170
|
serves — call it once at wire-up if you want the assertion, never per request.
|
|
135
171
|
|
|
@@ -147,7 +183,7 @@ that passes at 5.99 MB can still be rejected upstream. Lower
|
|
|
147
183
|
### 4. Shadow mode is a one-line switch
|
|
148
184
|
|
|
149
185
|
```python
|
|
150
|
-
client = NeedlepathClient(operating_point="np-2026-
|
|
186
|
+
client = NeedlepathClient(operating_point="np-2026-08-r4", shadow=True)
|
|
151
187
|
```
|
|
152
188
|
|
|
153
189
|
The call is made exactly as a live call would be — same body, same billing, same
|
|
@@ -193,13 +229,57 @@ carry fragments of your input in an exception message — the Lambda deliberatel
|
|
|
193
229
|
declines to log it for that reason — so only its *type* appears here. The full
|
|
194
230
|
string stays on `result.response.selection_error`.
|
|
195
231
|
|
|
232
|
+
## System One models (TypeSafe Jev, Laya)
|
|
233
|
+
|
|
234
|
+
Small classifier models that take `{state, questions}` and answer the questions
|
|
235
|
+
against the state. `needlepath.integrations.system_one` reshapes a selection
|
|
236
|
+
into that request body:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from needlepath.integrations.system_one import selection_receipt, to_system_one_state
|
|
240
|
+
|
|
241
|
+
result = client.select(records=records, task=task, max_context_tokens=4000)
|
|
242
|
+
payload = to_system_one_state(
|
|
243
|
+
# Fail open with your own records: a result that did not apply is refused.
|
|
244
|
+
result if result.applied else records,
|
|
245
|
+
questions={"refund": Noul(instructions="Is this a refund request?")},
|
|
246
|
+
)
|
|
247
|
+
# Sidecar metadata, not part of the body.
|
|
248
|
+
receipt = selection_receipt(result)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`payload["state"]` is a JSON array of `{record_id, title, source, text}`, one
|
|
252
|
+
entry per record in `response.selected`, **in the order the service returned
|
|
253
|
+
them, with the text unmodified**. The adapter is transport only: no threshold on
|
|
254
|
+
`score`, no drop for an empty excerpt, no re-sort, no deduplication, and no
|
|
255
|
+
question you did not pass. `mode="rendered"` returns `state` as a single string
|
|
256
|
+
instead, records joined by blank lines under a one-line `[record_id] title`
|
|
257
|
+
header, for a classifier whose state argument is text.
|
|
258
|
+
|
|
259
|
+
`questions` travels in the shape you pass it. `langchain-typesafe` reads a
|
|
260
|
+
mapping of id to typed question (`Noul`, `Choice`, `Score`); a different
|
|
261
|
+
`{state, questions}` service may read a list. Both are copied and sent
|
|
262
|
+
unchanged, so a mapping never arrives as a list of its keys. A bare string is
|
|
263
|
+
refused rather than iterated into single-character questions.
|
|
264
|
+
|
|
265
|
+
`to_system_one_state` accepts a `SelectionResult` only when it `applied`. A
|
|
266
|
+
result that did not apply is a passthrough — your own records are what has to
|
|
267
|
+
be sent — and its `response` can still carry a selection the client itself
|
|
268
|
+
declined to use, so passing one raises `NeedlepathConfigError` naming the
|
|
269
|
+
reason. To reshape that response anyway, for an audit or a shadow report, pass
|
|
270
|
+
`result.response`.
|
|
271
|
+
|
|
272
|
+
`selection_receipt()` returns `request_id`, `selection_id`, `policy_version`,
|
|
273
|
+
`tokens_saved` and `outcome`, prefixed the way `result.metadata()` prefixes its
|
|
274
|
+
keys. Attribution travels beside the request; record text never travels in it.
|
|
275
|
+
|
|
196
276
|
## Testing your integration
|
|
197
277
|
|
|
198
278
|
```python
|
|
199
279
|
from needlepath.testing import FakeTransport, response_payload, ok, error
|
|
200
280
|
|
|
201
281
|
transport = FakeTransport([error(503), ok(response_payload("np-1"))])
|
|
202
|
-
client = NeedlepathClient(operating_point="np-2026-
|
|
282
|
+
client = NeedlepathClient(operating_point="np-2026-08-r4", transport=transport)
|
|
203
283
|
```
|
|
204
284
|
|
|
205
285
|
`needlepath.testing` is public API, not a private helper. Every rule above is a
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "needlepath"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Framework-free client for the Needlepath hosted context-selection service."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -52,6 +52,7 @@ packages = ["src/needlepath"]
|
|
|
52
52
|
[tool.pytest.ini_options]
|
|
53
53
|
testpaths = ["tests"]
|
|
54
54
|
asyncio_mode = "auto"
|
|
55
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
55
56
|
filterwarnings = ["error"]
|
|
56
57
|
|
|
57
58
|
[tool.ruff]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from needlepath import NeedlepathClient, ContextRecord, TaskSpec
|
|
4
4
|
|
|
5
|
-
client = NeedlepathClient(operating_point="np-2026-
|
|
5
|
+
client = NeedlepathClient(operating_point="np-2026-08-r4") # key from env
|
|
6
6
|
result = client.select(
|
|
7
7
|
records=[ContextRecord(text=tool_output, kind="tool_result", id="t1")],
|
|
8
8
|
task=TaskSpec(prompt=user_question),
|
|
@@ -33,10 +33,13 @@ from .client import (
|
|
|
33
33
|
HEALTH_PATH,
|
|
34
34
|
MAX_REQUEST_BYTES,
|
|
35
35
|
OPERATING_POINTS_PATH,
|
|
36
|
+
OUTCOME_RETRY_STATUSES,
|
|
37
|
+
OUTCOMES_PATH,
|
|
36
38
|
RETRY_STATUSES,
|
|
37
39
|
SELECT_PATH,
|
|
38
40
|
AsyncNeedlepathClient,
|
|
39
41
|
NeedlepathClient,
|
|
42
|
+
build_outcome_report,
|
|
40
43
|
build_request,
|
|
41
44
|
classify,
|
|
42
45
|
encode_body,
|
|
@@ -61,9 +64,14 @@ from .models import (
|
|
|
61
64
|
ContextRequest,
|
|
62
65
|
ContextResponse,
|
|
63
66
|
GateSummary,
|
|
67
|
+
OutcomeReport,
|
|
68
|
+
OutcomeResponse,
|
|
64
69
|
RecordKind,
|
|
65
70
|
SafetySummary,
|
|
71
|
+
SecurityMode,
|
|
66
72
|
SelectedRecord,
|
|
73
|
+
SelectionTrace,
|
|
74
|
+
SelectionTraceRecord,
|
|
67
75
|
TaskSpec,
|
|
68
76
|
)
|
|
69
77
|
from .result import BENIGN_REASONS, Reason, SelectionResult, SelectionStats
|
|
@@ -93,7 +101,12 @@ __all__ = [
|
|
|
93
101
|
"SelectedRecord",
|
|
94
102
|
"SafetySummary",
|
|
95
103
|
"GateSummary",
|
|
104
|
+
"SelectionTrace",
|
|
105
|
+
"SelectionTraceRecord",
|
|
106
|
+
"OutcomeReport",
|
|
107
|
+
"OutcomeResponse",
|
|
96
108
|
"RecordKind",
|
|
109
|
+
"SecurityMode",
|
|
97
110
|
"REQUIRED_RESPONSE_FIELDS",
|
|
98
111
|
# results
|
|
99
112
|
"SelectionResult",
|
|
@@ -118,6 +131,7 @@ __all__ = [
|
|
|
118
131
|
"default_async_transport",
|
|
119
132
|
# helpers
|
|
120
133
|
"build_request",
|
|
134
|
+
"build_outcome_report",
|
|
121
135
|
"encode_body",
|
|
122
136
|
"classify",
|
|
123
137
|
"validate_response_payload",
|
|
@@ -125,12 +139,14 @@ __all__ = [
|
|
|
125
139
|
# constants
|
|
126
140
|
"DEFAULT_BASE_URL",
|
|
127
141
|
"SELECT_PATH",
|
|
142
|
+
"OUTCOMES_PATH",
|
|
128
143
|
"HEALTH_PATH",
|
|
129
144
|
"OPERATING_POINTS_PATH",
|
|
130
145
|
"MAX_REQUEST_BYTES",
|
|
131
146
|
"DEFAULT_TIMEOUT_S",
|
|
132
147
|
"DEFAULT_MAX_RETRIES",
|
|
133
148
|
"RETRY_STATUSES",
|
|
149
|
+
"OUTCOME_RETRY_STATUSES",
|
|
134
150
|
# feature flags
|
|
135
151
|
"WIRE_SHADOW_MODE",
|
|
136
152
|
"wire_shadow_mode_enabled",
|