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.
Files changed (92) hide show
  1. hajer-0.1.0/LICENSE +21 -0
  2. hajer-0.1.0/PKG-INFO +213 -0
  3. hajer-0.1.0/README.md +175 -0
  4. hajer-0.1.0/hajer/__init__.py +148 -0
  5. hajer-0.1.0/hajer/__main__.py +344 -0
  6. hajer-0.1.0/hajer/_attach.py +426 -0
  7. hajer-0.1.0/hajer/_bootstrap/__init__.py +5 -0
  8. hajer-0.1.0/hajer/_bootstrap/sitecustomize.py +33 -0
  9. hajer-0.1.0/hajer/_boundary.py +194 -0
  10. hajer-0.1.0/hajer/_case_key.py +56 -0
  11. hajer-0.1.0/hajer/_checksums.py +105 -0
  12. hajer-0.1.0/hajer/_ci_prices.py +81 -0
  13. hajer-0.1.0/hajer/_ci_spend.py +181 -0
  14. hajer-0.1.0/hajer/_ci_usage.py +95 -0
  15. hajer-0.1.0/hajer/_claims.py +200 -0
  16. hajer-0.1.0/hajer/_client.py +656 -0
  17. hajer-0.1.0/hajer/_controls.py +218 -0
  18. hajer-0.1.0/hajer/_errors.py +77 -0
  19. hajer-0.1.0/hajer/_frame_context.py +110 -0
  20. hajer-0.1.0/hajer/_frames.py +405 -0
  21. hajer-0.1.0/hajer/_http_capture.py +414 -0
  22. hajer-0.1.0/hajer/_http_libraries.py +124 -0
  23. hajer-0.1.0/hajer/_instrumentation.py +240 -0
  24. hajer-0.1.0/hajer/_json.py +17 -0
  25. hajer-0.1.0/hajer/_jsonpath.py +173 -0
  26. hajer-0.1.0/hajer/_models.py +176 -0
  27. hajer-0.1.0/hajer/_observe_sink.py +135 -0
  28. hajer-0.1.0/hajer/_otel.py +89 -0
  29. hajer-0.1.0/hajer/_paths.py +30 -0
  30. hajer-0.1.0/hajer/_payload.py +273 -0
  31. hajer-0.1.0/hajer/_proxy.py +624 -0
  32. hajer-0.1.0/hajer/_queue.py +480 -0
  33. hajer-0.1.0/hajer/_raw_response.py +146 -0
  34. hajer-0.1.0/hajer/_reads.py +184 -0
  35. hajer-0.1.0/hajer/_redact.py +468 -0
  36. hajer-0.1.0/hajer/_replay_marker.py +34 -0
  37. hajer-0.1.0/hajer/_rules.py +259 -0
  38. hajer-0.1.0/hajer/_self_check.py +87 -0
  39. hajer-0.1.0/hajer/_settings.py +504 -0
  40. hajer-0.1.0/hajer/_stream.py +409 -0
  41. hajer-0.1.0/hajer/_supported.py +37 -0
  42. hajer-0.1.0/hajer/_targets.py +138 -0
  43. hajer-0.1.0/hajer/_targets_genai.py +194 -0
  44. hajer-0.1.0/hajer/_targets_litellm.py +95 -0
  45. hajer-0.1.0/hajer/_temporary.py +112 -0
  46. hajer-0.1.0/hajer/_transport.py +177 -0
  47. hajer-0.1.0/hajer/_upload.py +56 -0
  48. hajer-0.1.0/hajer/_verify_adapters.py +327 -0
  49. hajer-0.1.0/hajer/_vocabulary.py +110 -0
  50. hajer-0.1.0/hajer/_wire.py +366 -0
  51. hajer-0.1.0/hajer/_wrap.py +2096 -0
  52. hajer-0.1.0/hajer/autoattach.py +28 -0
  53. hajer-0.1.0/hajer/py.typed +0 -0
  54. hajer-0.1.0/hajer/pytest_plugin/__init__.py +401 -0
  55. hajer-0.1.0/hajer/pytest_plugin/_adapters.py +132 -0
  56. hajer-0.1.0/hajer/pytest_plugin/_boot.py +31 -0
  57. hajer-0.1.0/hajer/pytest_plugin/_child.py +108 -0
  58. hajer-0.1.0/hajer/pytest_plugin/_documents.py +200 -0
  59. hajer-0.1.0/hajer/pytest_plugin/_evaluate.py +264 -0
  60. hajer-0.1.0/hajer/pytest_plugin/_fixed_repeats.py +256 -0
  61. hajer-0.1.0/hajer/pytest_plugin/_grounding.py +153 -0
  62. hajer-0.1.0/hajer/pytest_plugin/_judge.py +72 -0
  63. hajer-0.1.0/hajer/pytest_plugin/_load.py +81 -0
  64. hajer-0.1.0/hajer/pytest_plugin/_metamorphic.py +67 -0
  65. hajer-0.1.0/hajer/pytest_plugin/_models.py +137 -0
  66. hajer-0.1.0/hajer/pytest_plugin/_policy.py +49 -0
  67. hajer-0.1.0/hajer/pytest_plugin/_private_inputs.py +89 -0
  68. hajer-0.1.0/hajer/pytest_plugin/_qualification.py +53 -0
  69. hajer-0.1.0/hajer/pytest_plugin/_request_fingerprint.py +21 -0
  70. hajer-0.1.0/hajer/pytest_plugin/_run.py +302 -0
  71. hajer-0.1.0/hajer/pytest_plugin/_situations.py +136 -0
  72. hajer-0.1.0/hajer/pytest_plugin/_upload.py +5 -0
  73. hajer-0.1.0/hajer/pytest_plugin/_values.py +31 -0
  74. hajer-0.1.0/hajer/replay/__init__.py +11 -0
  75. hajer-0.1.0/hajer/replay/__main__.py +94 -0
  76. hajer-0.1.0/hajer/replay/_boot.py +43 -0
  77. hajer-0.1.0/hajer/replay/_capture.py +201 -0
  78. hajer-0.1.0/hajer/replay/_case.py +178 -0
  79. hajer-0.1.0/hajer/replay/_coerce.py +68 -0
  80. hajer-0.1.0/hajer/replay/_database.py +59 -0
  81. hajer-0.1.0/hajer/replay/_entry.py +158 -0
  82. hajer-0.1.0/hajer/replay/_entry_errors.py +13 -0
  83. hajer-0.1.0/hajer/replay/_fake.py +105 -0
  84. hajer-0.1.0/hajer/replay/_guard.py +556 -0
  85. hajer-0.1.0/hajer/replay/_hooks.py +134 -0
  86. hajer-0.1.0/hajer/replay/_platform.py +126 -0
  87. hajer-0.1.0/hajer/replay/_reach.py +145 -0
  88. hajer-0.1.0/hajer/replay/_reach_boot.py +32 -0
  89. hajer-0.1.0/hajer/replay/_recipes.py +143 -0
  90. hajer-0.1.0/hajer/replay/_standin.py +121 -0
  91. hajer-0.1.0/pyproject.toml +222 -0
  92. 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
+ ]