hajer 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.
- hajer-0.1.0/LICENSE +21 -0
- hajer-0.1.0/PKG-INFO +213 -0
- hajer-0.1.0/README.md +175 -0
- hajer-0.1.0/hajer/__init__.py +148 -0
- hajer-0.1.0/hajer/__main__.py +344 -0
- hajer-0.1.0/hajer/_attach.py +426 -0
- hajer-0.1.0/hajer/_bootstrap/__init__.py +5 -0
- hajer-0.1.0/hajer/_bootstrap/sitecustomize.py +33 -0
- hajer-0.1.0/hajer/_boundary.py +194 -0
- hajer-0.1.0/hajer/_case_key.py +56 -0
- hajer-0.1.0/hajer/_checksums.py +105 -0
- hajer-0.1.0/hajer/_ci_prices.py +81 -0
- hajer-0.1.0/hajer/_ci_spend.py +181 -0
- hajer-0.1.0/hajer/_ci_usage.py +95 -0
- hajer-0.1.0/hajer/_claims.py +200 -0
- hajer-0.1.0/hajer/_client.py +656 -0
- hajer-0.1.0/hajer/_controls.py +218 -0
- hajer-0.1.0/hajer/_errors.py +77 -0
- hajer-0.1.0/hajer/_frame_context.py +110 -0
- hajer-0.1.0/hajer/_frames.py +405 -0
- hajer-0.1.0/hajer/_http_capture.py +414 -0
- hajer-0.1.0/hajer/_http_libraries.py +124 -0
- hajer-0.1.0/hajer/_instrumentation.py +240 -0
- hajer-0.1.0/hajer/_json.py +17 -0
- hajer-0.1.0/hajer/_jsonpath.py +173 -0
- hajer-0.1.0/hajer/_models.py +176 -0
- hajer-0.1.0/hajer/_observe_sink.py +135 -0
- hajer-0.1.0/hajer/_otel.py +89 -0
- hajer-0.1.0/hajer/_paths.py +30 -0
- hajer-0.1.0/hajer/_payload.py +273 -0
- hajer-0.1.0/hajer/_proxy.py +624 -0
- hajer-0.1.0/hajer/_queue.py +480 -0
- hajer-0.1.0/hajer/_raw_response.py +146 -0
- hajer-0.1.0/hajer/_reads.py +184 -0
- hajer-0.1.0/hajer/_redact.py +468 -0
- hajer-0.1.0/hajer/_replay_marker.py +34 -0
- hajer-0.1.0/hajer/_rules.py +259 -0
- hajer-0.1.0/hajer/_self_check.py +87 -0
- hajer-0.1.0/hajer/_settings.py +504 -0
- hajer-0.1.0/hajer/_stream.py +409 -0
- hajer-0.1.0/hajer/_supported.py +37 -0
- hajer-0.1.0/hajer/_targets.py +138 -0
- hajer-0.1.0/hajer/_targets_genai.py +194 -0
- hajer-0.1.0/hajer/_targets_litellm.py +95 -0
- hajer-0.1.0/hajer/_temporary.py +112 -0
- hajer-0.1.0/hajer/_transport.py +177 -0
- hajer-0.1.0/hajer/_upload.py +56 -0
- hajer-0.1.0/hajer/_verify_adapters.py +327 -0
- hajer-0.1.0/hajer/_vocabulary.py +110 -0
- hajer-0.1.0/hajer/_wire.py +366 -0
- hajer-0.1.0/hajer/_wrap.py +2096 -0
- hajer-0.1.0/hajer/autoattach.py +28 -0
- hajer-0.1.0/hajer/py.typed +0 -0
- hajer-0.1.0/hajer/pytest_plugin/__init__.py +401 -0
- hajer-0.1.0/hajer/pytest_plugin/_adapters.py +132 -0
- hajer-0.1.0/hajer/pytest_plugin/_boot.py +31 -0
- hajer-0.1.0/hajer/pytest_plugin/_child.py +108 -0
- hajer-0.1.0/hajer/pytest_plugin/_documents.py +200 -0
- hajer-0.1.0/hajer/pytest_plugin/_evaluate.py +264 -0
- hajer-0.1.0/hajer/pytest_plugin/_fixed_repeats.py +256 -0
- hajer-0.1.0/hajer/pytest_plugin/_grounding.py +153 -0
- hajer-0.1.0/hajer/pytest_plugin/_judge.py +72 -0
- hajer-0.1.0/hajer/pytest_plugin/_load.py +81 -0
- hajer-0.1.0/hajer/pytest_plugin/_metamorphic.py +67 -0
- hajer-0.1.0/hajer/pytest_plugin/_models.py +137 -0
- hajer-0.1.0/hajer/pytest_plugin/_policy.py +49 -0
- hajer-0.1.0/hajer/pytest_plugin/_private_inputs.py +89 -0
- hajer-0.1.0/hajer/pytest_plugin/_qualification.py +53 -0
- hajer-0.1.0/hajer/pytest_plugin/_request_fingerprint.py +21 -0
- hajer-0.1.0/hajer/pytest_plugin/_run.py +302 -0
- hajer-0.1.0/hajer/pytest_plugin/_situations.py +136 -0
- hajer-0.1.0/hajer/pytest_plugin/_upload.py +5 -0
- hajer-0.1.0/hajer/pytest_plugin/_values.py +31 -0
- hajer-0.1.0/hajer/replay/__init__.py +11 -0
- hajer-0.1.0/hajer/replay/__main__.py +94 -0
- hajer-0.1.0/hajer/replay/_boot.py +43 -0
- hajer-0.1.0/hajer/replay/_capture.py +201 -0
- hajer-0.1.0/hajer/replay/_case.py +178 -0
- hajer-0.1.0/hajer/replay/_coerce.py +68 -0
- hajer-0.1.0/hajer/replay/_database.py +59 -0
- hajer-0.1.0/hajer/replay/_entry.py +158 -0
- hajer-0.1.0/hajer/replay/_entry_errors.py +13 -0
- hajer-0.1.0/hajer/replay/_fake.py +105 -0
- hajer-0.1.0/hajer/replay/_guard.py +556 -0
- hajer-0.1.0/hajer/replay/_hooks.py +134 -0
- hajer-0.1.0/hajer/replay/_platform.py +126 -0
- hajer-0.1.0/hajer/replay/_reach.py +145 -0
- hajer-0.1.0/hajer/replay/_reach_boot.py +32 -0
- hajer-0.1.0/hajer/replay/_recipes.py +143 -0
- hajer-0.1.0/hajer/replay/_standin.py +121 -0
- hajer-0.1.0/pyproject.toml +222 -0
- hajer-0.1.0/pyproject.toml.orig +225 -0
hajer-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hajer
|
|
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.
|
hajer-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hajer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The Hajer SDK: hand a verifier the request, the output and the evidence, get an assessment back.
|
|
5
|
+
Keywords: llm,ai,verification,guardrails,evaluation,observability,openai,anthropic
|
|
6
|
+
Author: Hajer
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Framework :: Pytest
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Dist: httpx>=0.28.1
|
|
22
|
+
Requires-Dist: pydantic>=2.12
|
|
23
|
+
Requires-Dist: anthropic>=0.75.0 ; extra == 'anthropic'
|
|
24
|
+
Requires-Dist: pytest>=9.1.1 ; extra == 'ci'
|
|
25
|
+
Requires-Dist: openai>=3.13.0 ; extra == 'openai'
|
|
26
|
+
Requires-Dist: opentelemetry-sdk>=1.37.0 ; extra == 'otel'
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Project-URL: Homepage, https://hajer.ai
|
|
29
|
+
Project-URL: Repository, https://github.com/HajerAI/hajer-sdk
|
|
30
|
+
Project-URL: Documentation, https://github.com/HajerAI/hajer-sdk/tree/main/python/docs
|
|
31
|
+
Project-URL: Issues, https://github.com/HajerAI/hajer-sdk/issues
|
|
32
|
+
Project-URL: Changelog, https://github.com/HajerAI/hajer-sdk/blob/main/python/CHANGELOG.md
|
|
33
|
+
Provides-Extra: anthropic
|
|
34
|
+
Provides-Extra: ci
|
|
35
|
+
Provides-Extra: openai
|
|
36
|
+
Provides-Extra: otel
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# hajer — the Python SDK
|
|
40
|
+
|
|
41
|
+
At the point where a model output crosses into a side effect — an HTTP response, an email, a database
|
|
42
|
+
write — hand Hajer the request, the output and the evidence you chose. Hajer applies a named, versioned
|
|
43
|
+
**verifier** and returns an **assessment**. Your application decides what to do with it.
|
|
44
|
+
|
|
45
|
+
This package is that call. It does not run your application, does not sit in your provider path, and
|
|
46
|
+
does not decide anything on your behalf. It can also record the model calls your application makes, so
|
|
47
|
+
an assessment is read beside the calls that produced the output.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install hajer # httpx + pydantic, nothing else
|
|
53
|
+
pip install "hajer[openai]" # with the OpenAI SDK alongside
|
|
54
|
+
pip install "hajer[anthropic]" # with the Anthropic SDK alongside
|
|
55
|
+
pip install "hajer[otel]" # optional coexistence with an existing OpenTelemetry setup
|
|
56
|
+
pip install "hajer[ci]" # the pytest plugin that runs Hajer suites in CI
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The `openai` and `anthropic` extras are a convenience: the SDK never imports either library.
|
|
60
|
+
`wrap(client)` instruments the object you hand it, by attribute.
|
|
61
|
+
|
|
62
|
+
Python ≥ 3.11, `httpx>=0.28.1`, `pydantic>=2.12`. The pydantic floor is deliberately low: this package
|
|
63
|
+
installs into **your** environment, and a floor above your pin would make it uninstallable rather than
|
|
64
|
+
make you upgrade.
|
|
65
|
+
|
|
66
|
+
## Configure
|
|
67
|
+
|
|
68
|
+
Three environment variables, read once when the client is constructed:
|
|
69
|
+
|
|
70
|
+
| Variable | Required | Meaning |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `HAJER_API_KEY` | yes | Your team API key. |
|
|
73
|
+
| `HAJER_TEAM_ID` | yes | The team every request is scoped to. |
|
|
74
|
+
| `HAJER_BASE_URL` | no | The service. Defaults to `https://api.hajer.ai`; set it only for a local or self-hosted platform. |
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
export HAJER_API_KEY=...
|
|
78
|
+
export HAJER_TEAM_ID=...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Getting a key.** In the Hajer app, Settings → API keys → Create key. The key is shown once, together
|
|
82
|
+
with the team id and the base URL as a `.env` block you can copy as is; the team id stays beside the
|
|
83
|
+
page's title afterwards. If you do not have access to a team yet, contact the Hajer team.
|
|
84
|
+
|
|
85
|
+
**Inert without a key.** Without `HAJER_API_KEY` and `HAJER_TEAM_ID` — or with `HAJER_DISABLED=1` — the
|
|
86
|
+
client is inert: `verify` returns `Assessment(status="unavailable", reason="DISABLED")` immediately with
|
|
87
|
+
no socket, `observe` returns a receipt in state `disabled`, and nothing raises. The integration can land
|
|
88
|
+
in a repository whose test suite has no Hajer credentials and pass unchanged. A missing key is not a
|
|
89
|
+
configuration error.
|
|
90
|
+
|
|
91
|
+
Set `HAJER_ENVIRONMENT` (`production`, `staging`, `dev`) in every environment you want Hajer to learn
|
|
92
|
+
from. Every other setting has a default; the full table is in the
|
|
93
|
+
[reference](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/reference.md#settings).
|
|
94
|
+
|
|
95
|
+
## Quickstart
|
|
96
|
+
|
|
97
|
+
<!-- The `hajer:quickstart` tag on the fence is extracted and compiled by an external check. Keep the
|
|
98
|
+
tag, and keep the block valid Python: a copy kept elsewhere would pass forever while these lines broke. -->
|
|
99
|
+
|
|
100
|
+
```python hajer:quickstart
|
|
101
|
+
import hajer
|
|
102
|
+
from openai import OpenAI
|
|
103
|
+
|
|
104
|
+
hajer_client = hajer.Hajer() # reads HAJER_API_KEY and HAJER_TEAM_ID from the environment
|
|
105
|
+
openai_client = hajer.wrap(OpenAI()) # model calls are recorded and attached to the next verify
|
|
106
|
+
|
|
107
|
+
def handle_ticket(ticket, order):
|
|
108
|
+
reply = openai_client.chat.completions.create(
|
|
109
|
+
model="gpt-5", messages=[{"role": "user", "content": ticket.question}]
|
|
110
|
+
).choices[0].message.content
|
|
111
|
+
|
|
112
|
+
assessment = hajer_client.verify(
|
|
113
|
+
"refund-policy@1", # the verifier, pinned: there is no "latest"
|
|
114
|
+
{"ticketId": ticket.id, "question": ticket.question}, # the request
|
|
115
|
+
reply, # the output, as it will be sent
|
|
116
|
+
{"orderState": order.state, "approvedPolicy": order.policy}, # the evidence you chose
|
|
117
|
+
)
|
|
118
|
+
if assessment.status == "violated":
|
|
119
|
+
return hold_for_review(reply, assessment.findings)
|
|
120
|
+
return send(reply)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Three things about those lines, because they are the whole design:
|
|
124
|
+
|
|
125
|
+
- **Explicit evidence fields.** `{"orderState": order.state}`, never `order.to_dict()`. A verifier's
|
|
126
|
+
evidence contract names fields; an assessment can only be about what it was given.
|
|
127
|
+
- **The final payload, before the effect.** `verify` is called on the string that is about to be sent,
|
|
128
|
+
after every transformation, not on the raw model output.
|
|
129
|
+
- **The application decides.** `verify` answers; your `if` acts. Nothing in this SDK holds, retries or
|
|
130
|
+
sends on your behalf.
|
|
131
|
+
|
|
132
|
+
`assessment.status` is one of `satisfied`, `violated`, `insufficient_evidence` or `unavailable`.
|
|
133
|
+
|
|
134
|
+
To try one `verify` end to end against a verifier that already exists, see
|
|
135
|
+
[Your first row](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/local-platform.md#your-first-row).
|
|
136
|
+
|
|
137
|
+
## Concepts
|
|
138
|
+
|
|
139
|
+
**`verify` and `observe`.** `verify` is synchronous with your request path: it returns an `Assessment`
|
|
140
|
+
within `deadline_ms` (default 1500 ms) and **never raises** — a transport failure, a timeout or a 5xx is
|
|
141
|
+
an `unavailable` assessment whose `reason` says which. There is no automatic retry, because a late answer
|
|
142
|
+
cannot justify an effect you already performed. `observe` takes the same arguments, puts the submission
|
|
143
|
+
on a bounded in-memory queue and returns a receipt at once; a background worker flushes it, and the
|
|
144
|
+
receipt moves through `queued`, `accepted` and `complete`. Use `observe` where nothing waits on the
|
|
145
|
+
answer, including semantic checks too slow to run inline. `AsyncHajer` is the same client for asyncio.
|
|
146
|
+
|
|
147
|
+
**`wrap(client)`.** Instruments an OpenAI, Anthropic, google-genai or LangChain chat model client — or
|
|
148
|
+
the `litellm` module — in place and returns the same object. Each call it makes is recorded (provider,
|
|
149
|
+
model, settings, tool calls and results, usage, timing, and message content, redacted client-side) and
|
|
150
|
+
attached as `wrappedCalls` to the next `verify` or `observe` in the same task. `hajer.instrument()` does
|
|
151
|
+
the same for clients constructed later, when you cannot reach the construction site.
|
|
152
|
+
|
|
153
|
+
**`scope()`.** Frameworks often make provider calls in child asyncio tasks, whose context never reaches
|
|
154
|
+
the parent's `verify`. A scope collects every call made inside it, whatever task made it:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
with hajer.scope(workflow="support-answer"):
|
|
158
|
+
reply = await agent.ainvoke(question)
|
|
159
|
+
assessment = hajer_client.verify(...)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Attach mode.** For an application nobody has instrumented yet: every provider call made outside a
|
|
163
|
+
`scope()` becomes one `observe` observation of its own, with no verifier — which is how you find out what
|
|
164
|
+
the workflows are before writing an obligation about any of them. It is opt-in:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
PYTHONPATH="$(python -m hajer attach-path)" HAJER_ATTACH=1 python -m your_app # no code change
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
import hajer.autoattach # one line in the entry point; reads HAJER_ATTACH, so it is safe to keep
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Client-side redaction** is on by default. Card numbers, IBANs, national ids, credentials, email
|
|
175
|
+
addresses and similar shapes are replaced with `[redacted:<CATEGORY>]` before anything leaves the
|
|
176
|
+
process. `hajer.build_policy(...)` adjusts it per client or per call.
|
|
177
|
+
|
|
178
|
+
## Command line
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
python -m hajer doctor # every HAJER_* setting in force, where it came from, and whether the service answers
|
|
182
|
+
python -m hajer tail --follow # one line per recorded observation as it lands
|
|
183
|
+
python -m hajer proxy --upstream https://api.anthropic.com --listen 127.0.0.1:8091 # record at the wire
|
|
184
|
+
python -m hajer attach-path # the directory to put on PYTHONPATH for attach mode
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`doctor` is the first command to run when nothing is arriving. It never prints your key.
|
|
188
|
+
|
|
189
|
+
## Supported libraries
|
|
190
|
+
|
|
191
|
+
`openai`, `anthropic`, `langchain-openai`, `langchain-anthropic`, `litellm` and `google-genai`, sync and
|
|
192
|
+
async, streamed and not. The
|
|
193
|
+
[support matrix](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/support-matrix.md) is
|
|
194
|
+
generated from the SDK's own target declarations and lists, per library, whether usage is observable on
|
|
195
|
+
a streamed call and each one's caveat. A model request your code sends through its own httpx client
|
|
196
|
+
can be captured by wrapping that client's transport in `hajer.CaptureTransport`.
|
|
197
|
+
|
|
198
|
+
## Documentation
|
|
199
|
+
|
|
200
|
+
- [Reference](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/reference.md) — the public
|
|
201
|
+
API, the `verify` reason table, the `observe` delivery contract, idempotency and case keys, what
|
|
202
|
+
`wrap` captures, streaming, redaction, attach mode, the command line, every setting, costs and errors.
|
|
203
|
+
- [CI suites](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/ci.md) — the pytest plugin
|
|
204
|
+
that runs Hajer suites in your CI, and the
|
|
205
|
+
[GitHub Action](https://github.com/HajerAI/hajer-sdk/blob/main/action/README.md) that wraps it.
|
|
206
|
+
- [Local platform](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/local-platform.md) —
|
|
207
|
+
pointing the SDK at a local or self-hosted Hajer platform, and a first `verify` you can run as written.
|
|
208
|
+
- [Development](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/development.md) — working
|
|
209
|
+
on this package.
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT.
|
hajer-0.1.0/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# hajer — the Python SDK
|
|
2
|
+
|
|
3
|
+
At the point where a model output crosses into a side effect — an HTTP response, an email, a database
|
|
4
|
+
write — hand Hajer the request, the output and the evidence you chose. Hajer applies a named, versioned
|
|
5
|
+
**verifier** and returns an **assessment**. Your application decides what to do with it.
|
|
6
|
+
|
|
7
|
+
This package is that call. It does not run your application, does not sit in your provider path, and
|
|
8
|
+
does not decide anything on your behalf. It can also record the model calls your application makes, so
|
|
9
|
+
an assessment is read beside the calls that produced the output.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install hajer # httpx + pydantic, nothing else
|
|
15
|
+
pip install "hajer[openai]" # with the OpenAI SDK alongside
|
|
16
|
+
pip install "hajer[anthropic]" # with the Anthropic SDK alongside
|
|
17
|
+
pip install "hajer[otel]" # optional coexistence with an existing OpenTelemetry setup
|
|
18
|
+
pip install "hajer[ci]" # the pytest plugin that runs Hajer suites in CI
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The `openai` and `anthropic` extras are a convenience: the SDK never imports either library.
|
|
22
|
+
`wrap(client)` instruments the object you hand it, by attribute.
|
|
23
|
+
|
|
24
|
+
Python ≥ 3.11, `httpx>=0.28.1`, `pydantic>=2.12`. The pydantic floor is deliberately low: this package
|
|
25
|
+
installs into **your** environment, and a floor above your pin would make it uninstallable rather than
|
|
26
|
+
make you upgrade.
|
|
27
|
+
|
|
28
|
+
## Configure
|
|
29
|
+
|
|
30
|
+
Three environment variables, read once when the client is constructed:
|
|
31
|
+
|
|
32
|
+
| Variable | Required | Meaning |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `HAJER_API_KEY` | yes | Your team API key. |
|
|
35
|
+
| `HAJER_TEAM_ID` | yes | The team every request is scoped to. |
|
|
36
|
+
| `HAJER_BASE_URL` | no | The service. Defaults to `https://api.hajer.ai`; set it only for a local or self-hosted platform. |
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export HAJER_API_KEY=...
|
|
40
|
+
export HAJER_TEAM_ID=...
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Getting a key.** In the Hajer app, Settings → API keys → Create key. The key is shown once, together
|
|
44
|
+
with the team id and the base URL as a `.env` block you can copy as is; the team id stays beside the
|
|
45
|
+
page's title afterwards. If you do not have access to a team yet, contact the Hajer team.
|
|
46
|
+
|
|
47
|
+
**Inert without a key.** Without `HAJER_API_KEY` and `HAJER_TEAM_ID` — or with `HAJER_DISABLED=1` — the
|
|
48
|
+
client is inert: `verify` returns `Assessment(status="unavailable", reason="DISABLED")` immediately with
|
|
49
|
+
no socket, `observe` returns a receipt in state `disabled`, and nothing raises. The integration can land
|
|
50
|
+
in a repository whose test suite has no Hajer credentials and pass unchanged. A missing key is not a
|
|
51
|
+
configuration error.
|
|
52
|
+
|
|
53
|
+
Set `HAJER_ENVIRONMENT` (`production`, `staging`, `dev`) in every environment you want Hajer to learn
|
|
54
|
+
from. Every other setting has a default; the full table is in the
|
|
55
|
+
[reference](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/reference.md#settings).
|
|
56
|
+
|
|
57
|
+
## Quickstart
|
|
58
|
+
|
|
59
|
+
<!-- The `hajer:quickstart` tag on the fence is extracted and compiled by an external check. Keep the
|
|
60
|
+
tag, and keep the block valid Python: a copy kept elsewhere would pass forever while these lines broke. -->
|
|
61
|
+
|
|
62
|
+
```python hajer:quickstart
|
|
63
|
+
import hajer
|
|
64
|
+
from openai import OpenAI
|
|
65
|
+
|
|
66
|
+
hajer_client = hajer.Hajer() # reads HAJER_API_KEY and HAJER_TEAM_ID from the environment
|
|
67
|
+
openai_client = hajer.wrap(OpenAI()) # model calls are recorded and attached to the next verify
|
|
68
|
+
|
|
69
|
+
def handle_ticket(ticket, order):
|
|
70
|
+
reply = openai_client.chat.completions.create(
|
|
71
|
+
model="gpt-5", messages=[{"role": "user", "content": ticket.question}]
|
|
72
|
+
).choices[0].message.content
|
|
73
|
+
|
|
74
|
+
assessment = hajer_client.verify(
|
|
75
|
+
"refund-policy@1", # the verifier, pinned: there is no "latest"
|
|
76
|
+
{"ticketId": ticket.id, "question": ticket.question}, # the request
|
|
77
|
+
reply, # the output, as it will be sent
|
|
78
|
+
{"orderState": order.state, "approvedPolicy": order.policy}, # the evidence you chose
|
|
79
|
+
)
|
|
80
|
+
if assessment.status == "violated":
|
|
81
|
+
return hold_for_review(reply, assessment.findings)
|
|
82
|
+
return send(reply)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Three things about those lines, because they are the whole design:
|
|
86
|
+
|
|
87
|
+
- **Explicit evidence fields.** `{"orderState": order.state}`, never `order.to_dict()`. A verifier's
|
|
88
|
+
evidence contract names fields; an assessment can only be about what it was given.
|
|
89
|
+
- **The final payload, before the effect.** `verify` is called on the string that is about to be sent,
|
|
90
|
+
after every transformation, not on the raw model output.
|
|
91
|
+
- **The application decides.** `verify` answers; your `if` acts. Nothing in this SDK holds, retries or
|
|
92
|
+
sends on your behalf.
|
|
93
|
+
|
|
94
|
+
`assessment.status` is one of `satisfied`, `violated`, `insufficient_evidence` or `unavailable`.
|
|
95
|
+
|
|
96
|
+
To try one `verify` end to end against a verifier that already exists, see
|
|
97
|
+
[Your first row](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/local-platform.md#your-first-row).
|
|
98
|
+
|
|
99
|
+
## Concepts
|
|
100
|
+
|
|
101
|
+
**`verify` and `observe`.** `verify` is synchronous with your request path: it returns an `Assessment`
|
|
102
|
+
within `deadline_ms` (default 1500 ms) and **never raises** — a transport failure, a timeout or a 5xx is
|
|
103
|
+
an `unavailable` assessment whose `reason` says which. There is no automatic retry, because a late answer
|
|
104
|
+
cannot justify an effect you already performed. `observe` takes the same arguments, puts the submission
|
|
105
|
+
on a bounded in-memory queue and returns a receipt at once; a background worker flushes it, and the
|
|
106
|
+
receipt moves through `queued`, `accepted` and `complete`. Use `observe` where nothing waits on the
|
|
107
|
+
answer, including semantic checks too slow to run inline. `AsyncHajer` is the same client for asyncio.
|
|
108
|
+
|
|
109
|
+
**`wrap(client)`.** Instruments an OpenAI, Anthropic, google-genai or LangChain chat model client — or
|
|
110
|
+
the `litellm` module — in place and returns the same object. Each call it makes is recorded (provider,
|
|
111
|
+
model, settings, tool calls and results, usage, timing, and message content, redacted client-side) and
|
|
112
|
+
attached as `wrappedCalls` to the next `verify` or `observe` in the same task. `hajer.instrument()` does
|
|
113
|
+
the same for clients constructed later, when you cannot reach the construction site.
|
|
114
|
+
|
|
115
|
+
**`scope()`.** Frameworks often make provider calls in child asyncio tasks, whose context never reaches
|
|
116
|
+
the parent's `verify`. A scope collects every call made inside it, whatever task made it:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
with hajer.scope(workflow="support-answer"):
|
|
120
|
+
reply = await agent.ainvoke(question)
|
|
121
|
+
assessment = hajer_client.verify(...)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Attach mode.** For an application nobody has instrumented yet: every provider call made outside a
|
|
125
|
+
`scope()` becomes one `observe` observation of its own, with no verifier — which is how you find out what
|
|
126
|
+
the workflows are before writing an obligation about any of them. It is opt-in:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
PYTHONPATH="$(python -m hajer attach-path)" HAJER_ATTACH=1 python -m your_app # no code change
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import hajer.autoattach # one line in the entry point; reads HAJER_ATTACH, so it is safe to keep
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Client-side redaction** is on by default. Card numbers, IBANs, national ids, credentials, email
|
|
137
|
+
addresses and similar shapes are replaced with `[redacted:<CATEGORY>]` before anything leaves the
|
|
138
|
+
process. `hajer.build_policy(...)` adjusts it per client or per call.
|
|
139
|
+
|
|
140
|
+
## Command line
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
python -m hajer doctor # every HAJER_* setting in force, where it came from, and whether the service answers
|
|
144
|
+
python -m hajer tail --follow # one line per recorded observation as it lands
|
|
145
|
+
python -m hajer proxy --upstream https://api.anthropic.com --listen 127.0.0.1:8091 # record at the wire
|
|
146
|
+
python -m hajer attach-path # the directory to put on PYTHONPATH for attach mode
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`doctor` is the first command to run when nothing is arriving. It never prints your key.
|
|
150
|
+
|
|
151
|
+
## Supported libraries
|
|
152
|
+
|
|
153
|
+
`openai`, `anthropic`, `langchain-openai`, `langchain-anthropic`, `litellm` and `google-genai`, sync and
|
|
154
|
+
async, streamed and not. The
|
|
155
|
+
[support matrix](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/support-matrix.md) is
|
|
156
|
+
generated from the SDK's own target declarations and lists, per library, whether usage is observable on
|
|
157
|
+
a streamed call and each one's caveat. A model request your code sends through its own httpx client
|
|
158
|
+
can be captured by wrapping that client's transport in `hajer.CaptureTransport`.
|
|
159
|
+
|
|
160
|
+
## Documentation
|
|
161
|
+
|
|
162
|
+
- [Reference](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/reference.md) — the public
|
|
163
|
+
API, the `verify` reason table, the `observe` delivery contract, idempotency and case keys, what
|
|
164
|
+
`wrap` captures, streaming, redaction, attach mode, the command line, every setting, costs and errors.
|
|
165
|
+
- [CI suites](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/ci.md) — the pytest plugin
|
|
166
|
+
that runs Hajer suites in your CI, and the
|
|
167
|
+
[GitHub Action](https://github.com/HajerAI/hajer-sdk/blob/main/action/README.md) that wraps it.
|
|
168
|
+
- [Local platform](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/local-platform.md) —
|
|
169
|
+
pointing the SDK at a local or self-hosted Hajer platform, and a first `verify` you can run as written.
|
|
170
|
+
- [Development](https://github.com/HajerAI/hajer-sdk/blob/main/python/docs/development.md) — working
|
|
171
|
+
on this package.
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Hajer — the verifier in your own code.
|
|
2
|
+
|
|
3
|
+
At the point where a model output crosses into a side effect, hand Hajer the request, the output and
|
|
4
|
+
the evidence you chose. Hajer applies a named, versioned **verifier** and returns an **assessment**.
|
|
5
|
+
Your application decides what to do with it.
|
|
6
|
+
|
|
7
|
+
import hajer
|
|
8
|
+
|
|
9
|
+
hajer_client = hajer.Hajer() # HAJER_API_KEY, HAJER_TEAM_ID from the environment
|
|
10
|
+
openai_client = hajer.wrap(OpenAI()) # the model calls are recorded beside the answer
|
|
11
|
+
|
|
12
|
+
reply = openai_client.chat.completions.create(model="…", messages=…)
|
|
13
|
+
assessment = hajer_client.verify(
|
|
14
|
+
"refund-policy", # the verifier
|
|
15
|
+
{"orderId": order.id, "question": question}, # the request
|
|
16
|
+
reply.choices[0].message.content, # the output, as it will be sent
|
|
17
|
+
{"orderState": order.state, "approvedPolicy": policy.text}, # the evidence you chose
|
|
18
|
+
)
|
|
19
|
+
if assessment.status == "violated":
|
|
20
|
+
... # your policy, your decision
|
|
21
|
+
|
|
22
|
+
Without `HAJER_API_KEY` and `HAJER_TEAM_ID` the client is inert: `verify` returns
|
|
23
|
+
`unavailable{reason: DISABLED}`, `observe` is a no-op, nothing opens a socket and nothing raises — so
|
|
24
|
+
a test suite with no Hajer credentials runs exactly as it did before the SDK was added.
|
|
25
|
+
|
|
26
|
+
`hajer.scope()` is the unit an obligation is about: every provider call made inside the block — including
|
|
27
|
+
the ones a framework makes in child tasks — belongs to it, and the `verify` inside the block carries them.
|
|
28
|
+
|
|
29
|
+
`hajer.attach()` (or `HAJER_ATTACH=1` with the import-time hook) is the other way round: no verifier, no
|
|
30
|
+
scope, one `observe` observation per provider call, for a process nobody has instrumented yet.
|
|
31
|
+
`hajer/_attach.py` states exactly what leaves the process at each capture setting.
|
|
32
|
+
|
|
33
|
+
`README.md` has every setting, the delivery contract of `observe`, and what `wrap` can and cannot
|
|
34
|
+
capture. The module docstrings say the same things where the code is.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from hajer._attach import Attachment, attach, attachment, detach
|
|
40
|
+
from hajer._boundary import record_boundaries
|
|
41
|
+
from hajer._client import AsyncHajer, Hajer
|
|
42
|
+
from hajer._errors import (
|
|
43
|
+
AssessmentUnavailableError,
|
|
44
|
+
BodyOverBoundError,
|
|
45
|
+
HajerConfigError,
|
|
46
|
+
HajerError,
|
|
47
|
+
UnsupportedClientError,
|
|
48
|
+
)
|
|
49
|
+
from hajer._http_capture import AsyncCaptureTransport, CaptureTransport
|
|
50
|
+
from hajer._instrumentation import Instrumentation, instrument, uninstrument
|
|
51
|
+
from hajer._json import JsonObject, JsonValue
|
|
52
|
+
from hajer._models import (
|
|
53
|
+
Assessment,
|
|
54
|
+
CheckOutcome,
|
|
55
|
+
CheckReason,
|
|
56
|
+
Finding,
|
|
57
|
+
MissingEvidence,
|
|
58
|
+
MissingEvidenceReason,
|
|
59
|
+
ObservationRow,
|
|
60
|
+
UnavailableReason,
|
|
61
|
+
VerificationStatus,
|
|
62
|
+
)
|
|
63
|
+
from hajer._observe_sink import ObservationFileSink
|
|
64
|
+
from hajer._observe_sink import install_from_env as _install_observe_sink
|
|
65
|
+
from hajer._observe_sink import installed as observe_sink
|
|
66
|
+
from hajer._paths import VERSION
|
|
67
|
+
from hajer._payload import wire_source
|
|
68
|
+
from hajer._queue import AsyncObserveReceipt, DropReason, ObserveReceipt, ReceiptState
|
|
69
|
+
from hajer._redact import (
|
|
70
|
+
ClientRedactionPolicy,
|
|
71
|
+
RedactionEntry,
|
|
72
|
+
build_policy,
|
|
73
|
+
redact_document,
|
|
74
|
+
)
|
|
75
|
+
from hajer._settings import HajerSettings
|
|
76
|
+
from hajer._wrap import (
|
|
77
|
+
Operation,
|
|
78
|
+
ToolCall,
|
|
79
|
+
ToolResult,
|
|
80
|
+
WrappedCall,
|
|
81
|
+
clear_wrapped_calls,
|
|
82
|
+
scope,
|
|
83
|
+
wrap,
|
|
84
|
+
wrapped_calls,
|
|
85
|
+
wrapped_calls_dropped,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
__version__ = VERSION
|
|
89
|
+
|
|
90
|
+
#: The file sink is armed at import, because the process that needs it is the one nobody instrumented:
|
|
91
|
+
#: a test suite whose transports are patched imports the SDK through its own application and never
|
|
92
|
+
#: calls `attach()`. It is a no-op — no directory, no file, no hook — unless `HAJER_OBSERVE_SINK` names
|
|
93
|
+
#: a directory, and even then nothing is written until a call settles.
|
|
94
|
+
_SINK = _install_observe_sink()
|
|
95
|
+
|
|
96
|
+
#: The public API. Everything else in this package is private and starts with an underscore.
|
|
97
|
+
__all__ = [
|
|
98
|
+
"Assessment",
|
|
99
|
+
"AssessmentUnavailableError",
|
|
100
|
+
"AsyncCaptureTransport",
|
|
101
|
+
"AsyncHajer",
|
|
102
|
+
"AsyncObserveReceipt",
|
|
103
|
+
"Attachment",
|
|
104
|
+
"BodyOverBoundError",
|
|
105
|
+
"CaptureTransport",
|
|
106
|
+
"CheckOutcome",
|
|
107
|
+
"CheckReason",
|
|
108
|
+
"ClientRedactionPolicy",
|
|
109
|
+
"DropReason",
|
|
110
|
+
"Finding",
|
|
111
|
+
"Hajer",
|
|
112
|
+
"HajerConfigError",
|
|
113
|
+
"HajerError",
|
|
114
|
+
"HajerSettings",
|
|
115
|
+
"Instrumentation",
|
|
116
|
+
"JsonObject",
|
|
117
|
+
"JsonValue",
|
|
118
|
+
"MissingEvidence",
|
|
119
|
+
"MissingEvidenceReason",
|
|
120
|
+
"ObservationFileSink",
|
|
121
|
+
"ObservationRow",
|
|
122
|
+
"ObserveReceipt",
|
|
123
|
+
"Operation",
|
|
124
|
+
"ReceiptState",
|
|
125
|
+
"RedactionEntry",
|
|
126
|
+
"ToolCall",
|
|
127
|
+
"ToolResult",
|
|
128
|
+
"UnavailableReason",
|
|
129
|
+
"UnsupportedClientError",
|
|
130
|
+
"VerificationStatus",
|
|
131
|
+
"WrappedCall",
|
|
132
|
+
"__version__",
|
|
133
|
+
"attach",
|
|
134
|
+
"attachment",
|
|
135
|
+
"build_policy",
|
|
136
|
+
"clear_wrapped_calls",
|
|
137
|
+
"detach",
|
|
138
|
+
"instrument",
|
|
139
|
+
"observe_sink",
|
|
140
|
+
"record_boundaries",
|
|
141
|
+
"redact_document",
|
|
142
|
+
"scope",
|
|
143
|
+
"uninstrument",
|
|
144
|
+
"wire_source",
|
|
145
|
+
"wrap",
|
|
146
|
+
"wrapped_calls",
|
|
147
|
+
"wrapped_calls_dropped",
|
|
148
|
+
]
|