hgateway-sdk 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 (83) hide show
  1. hgateway_sdk-0.1.0/.github/workflows/publish-pypi.yml +28 -0
  2. hgateway_sdk-0.1.0/.github/workflows/release.yml +47 -0
  3. hgateway_sdk-0.1.0/.gitignore +21 -0
  4. hgateway_sdk-0.1.0/PKG-INFO +245 -0
  5. hgateway_sdk-0.1.0/README.md +222 -0
  6. hgateway_sdk-0.1.0/pyproject.toml +54 -0
  7. hgateway_sdk-0.1.0/src/hgateway_sdk/__init__.py +177 -0
  8. hgateway_sdk-0.1.0/src/hgateway_sdk/client/__init__.py +12 -0
  9. hgateway_sdk-0.1.0/src/hgateway_sdk/client/entrypoint.py +140 -0
  10. hgateway_sdk-0.1.0/src/hgateway_sdk/client/gateway_client.py +128 -0
  11. hgateway_sdk-0.1.0/src/hgateway_sdk/config/__init__.py +11 -0
  12. hgateway_sdk-0.1.0/src/hgateway_sdk/config/loader.py +79 -0
  13. hgateway_sdk-0.1.0/src/hgateway_sdk/constants/__init__.py +39 -0
  14. hgateway_sdk-0.1.0/src/hgateway_sdk/constants/field_type.py +34 -0
  15. hgateway_sdk-0.1.0/src/hgateway_sdk/constants/hitl_sub_type.py +78 -0
  16. hgateway_sdk-0.1.0/src/hgateway_sdk/constants/hitl_type.py +36 -0
  17. hgateway_sdk-0.1.0/src/hgateway_sdk/constants/on_expiry.py +28 -0
  18. hgateway_sdk-0.1.0/src/hgateway_sdk/constants/values.py +178 -0
  19. hgateway_sdk-0.1.0/src/hgateway_sdk/decorators.py +87 -0
  20. hgateway_sdk-0.1.0/src/hgateway_sdk/exceptions/__init__.py +35 -0
  21. hgateway_sdk-0.1.0/src/hgateway_sdk/exceptions/feature_validation_error.py +29 -0
  22. hgateway_sdk-0.1.0/src/hgateway_sdk/exceptions/gateway_bootstrap_error.py +27 -0
  23. hgateway_sdk-0.1.0/src/hgateway_sdk/exceptions/gateway_unreachable.py +31 -0
  24. hgateway_sdk-0.1.0/src/hgateway_sdk/exceptions/hgateway_error.py +16 -0
  25. hgateway_sdk-0.1.0/src/hgateway_sdk/exceptions/hitl_node_required.py +32 -0
  26. hgateway_sdk-0.1.0/src/hgateway_sdk/py.typed +0 -0
  27. hgateway_sdk-0.1.0/src/hgateway_sdk/runtime/__init__.py +19 -0
  28. hgateway_sdk-0.1.0/src/hgateway_sdk/runtime/context.py +142 -0
  29. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/__init__.py +117 -0
  30. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/ack.py +35 -0
  31. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/__init__.py +58 -0
  32. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/approval_content.py +31 -0
  33. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/binary_approval_content.py +55 -0
  34. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/confirm_context_content.py +46 -0
  35. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/content_edit_content.py +46 -0
  36. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/context_content.py +32 -0
  37. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/decision_content.py +32 -0
  38. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/edit_content.py +31 -0
  39. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/form_context_content.py +48 -0
  40. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/freetext_context_content.py +45 -0
  41. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/hitl_content.py +42 -0
  42. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/modify_approval_content.py +53 -0
  43. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/multi_decision_content.py +56 -0
  44. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/rank_decision_content.py +47 -0
  45. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/single_decision_content.py +45 -0
  46. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/tool_args_edit_content.py +52 -0
  47. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/__init__.py +28 -0
  48. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/choice.py +42 -0
  49. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/form_field.py +49 -0
  50. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/freetext_input.py +35 -0
  51. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/modifiable_field.py +52 -0
  52. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/option.py +45 -0
  53. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/tool_arg.py +49 -0
  54. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/envelope/__init__.py +11 -0
  55. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/envelope/hitl_spec.py +54 -0
  56. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/envelope/runtime_context.py +43 -0
  57. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/__init__.py +39 -0
  58. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/cross_feature_rule.py +15 -0
  59. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/feature.py +16 -0
  60. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/recipients.py +39 -0
  61. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/routing.py +46 -0
  62. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/run_features.py +54 -0
  63. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/ttl.py +65 -0
  64. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/ttl_forward_needs_secondary.py +27 -0
  65. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/validate_features.py +47 -0
  66. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/__init__.py +46 -0
  67. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/binary_approval_response.py +19 -0
  68. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/confirm_context_response.py +18 -0
  69. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/content_edit_response.py +20 -0
  70. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/form_context_response.py +19 -0
  71. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/freetext_context_response.py +17 -0
  72. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/modify_approval_response.py +23 -0
  73. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/multi_decision_response.py +19 -0
  74. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/rank_decision_response.py +19 -0
  75. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/single_decision_response.py +17 -0
  76. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/tool_args_edit_response.py +21 -0
  77. hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/settings.py +30 -0
  78. hgateway_sdk-0.1.0/src/hgateway_sdk/transport/__init__.py +15 -0
  79. hgateway_sdk-0.1.0/src/hgateway_sdk/transport/gateway_transport.py +54 -0
  80. hgateway_sdk-0.1.0/src/hgateway_sdk/transport/http_gateway_transport.py +135 -0
  81. hgateway_sdk-0.1.0/src/hgateway_sdk/utils/__init__.py +10 -0
  82. hgateway_sdk-0.1.0/src/hgateway_sdk/utils/serialization.py +99 -0
  83. hgateway_sdk-0.1.0/uv.lock +1436 -0
@@ -0,0 +1,28 @@
1
+ name: publish-pypi
2
+
3
+ # Fires once release.yml's tag-push job has already created the GitHub Release —
4
+ # decoupled from that workflow so publishing failures never block/duplicate the
5
+ # GitHub Release + artifact creation, and vice versa.
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ permissions:
11
+ contents: read # required for actions/checkout
12
+ id-token: write # required for PyPI trusted publishing (OIDC) — no stored token
13
+
14
+ jobs:
15
+ publish:
16
+ runs-on: ubuntu-latest
17
+ environment: pypi
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v5
23
+
24
+ - name: Build sdist + wheel
25
+ run: uv build
26
+
27
+ - name: Publish to PyPI
28
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,47 @@
1
+ name: release
2
+
3
+ # Build the wheel + sdist on every version tag (vX.Y.Z) and attach them to a
4
+ # GitHub Release. Consumers install the SDK either from the Release assets or
5
+ # directly from the git tag (see README "Install").
6
+ on:
7
+ push:
8
+ tags:
9
+ - "v*"
10
+
11
+ permissions:
12
+ contents: write # required to create the Release and upload assets
13
+
14
+ jobs:
15
+ build-and-release:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v5
22
+
23
+ - name: Verify tag matches project version
24
+ run: |
25
+ tag="${GITHUB_REF_NAME#v}"
26
+ proj="$(uv version --short 2>/dev/null || grep -m1 '^version' pyproject.toml | sed -E 's/.*"(.*)".*/\1/')"
27
+ echo "tag=$tag project=$proj"
28
+ if [ "$tag" != "$proj" ]; then
29
+ echo "::error::Tag v$tag does not match pyproject version $proj"
30
+ exit 1
31
+ fi
32
+
33
+ - name: Build sdist + wheel
34
+ run: uv build
35
+
36
+ - name: Smoke-check the built wheel imports
37
+ run: |
38
+ uv run --no-project --with dist/*.whl python -c "import hgateway_sdk; print(hgateway_sdk.__version__)"
39
+
40
+ - name: Create GitHub Release with artifacts
41
+ uses: softprops/action-gh-release@v2
42
+ with:
43
+ files: |
44
+ dist/*.whl
45
+ dist/*.tar.gz
46
+ fail_on_unmatched_files: true
47
+ generate_release_notes: true
@@ -0,0 +1,21 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .env
10
+ .idea/
11
+ .vscode/
12
+ .DS_Store
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+
17
+ # SRE test agent dev-server cache
18
+ tests/sre_agent/.langgraph_api/
19
+
20
+ .coverage
21
+ coverage.json
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: hgateway-sdk
3
+ Version: 0.1.0
4
+ Summary: theved.ai HITL Gateway SDK — raise human-in-the-loop interrupts from LangGraph nodes.
5
+ Project-URL: Homepage, https://theved.ai
6
+ Project-URL: Source, https://github.com/theved-ai/hgateway_sdk
7
+ Author: theved.ai
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: langchain-core>=1.0
18
+ Requires-Dist: langgraph>=1.0
19
+ Requires-Dist: python-dotenv>=1.2.2
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=8; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # hgateway-sdk
25
+
26
+ **Raise structured human-in-the-loop (HITL) interrupts from inside your LangGraph
27
+ agent and hand the human interaction to the theved.ai HITL Gateway.**
28
+
29
+ A thin client that carries the *ask* (a typed `HitlContent`) plus SDK-derived run
30
+ context, registers it out-of-band with the gateway, and wraps LangGraph's
31
+ `interrupt()` so the resumed value comes back as the paired typed response.
32
+
33
+ ## What it does / when to use it
34
+
35
+ Use it whenever a LangGraph node needs a human to approve, decide, edit, or supply
36
+ context before the graph continues.
37
+
38
+ The mental model is **ownership vs fallback**:
39
+
40
+ - When the gateway **accepts** an interrupt, it *owns* the human interaction —
41
+ routing, notification, TTL/escalation, and collecting the response. Your graph
42
+ simply suspends and later resumes with the operator's answer.
43
+ - When the gateway is **unreachable or declines**, the SDK falls back to a plain
44
+ local `interrupt(fallback)` so your own backend (or test harness) can resolve it.
45
+
46
+ This lets you adopt the gateway without giving up a working local path.
47
+
48
+ ## Install
49
+
50
+ The SDK is distributed from its (private) GitHub repository — there is no PyPI
51
+ release. Install it pinned to a release tag:
52
+
53
+ ```bash
54
+ uv add "hgateway-sdk @ git+https://github.com/theved-ai/hgateway_sdk.git@v0.1.0"
55
+ # or with pip:
56
+ pip install "hgateway-sdk @ git+https://github.com/theved-ai/hgateway_sdk.git@v0.1.0"
57
+ ```
58
+
59
+ Because the repo is private, the installing environment must be authenticated to
60
+ GitHub (an SSH key, or a token via `GH_TOKEN` / a credential helper / a
61
+ `git+https://<token>@github.com/...` URL).
62
+
63
+ Each release tag also has a built wheel + sdist attached as assets on its
64
+ [GitHub Release](https://github.com/theved-ai/hgateway_sdk/releases); you may
65
+ install directly from a downloaded wheel instead of building from source.
66
+
67
+ ## Configure
68
+
69
+ Settings are resolved from the process environment first; a `.env` file is consulted
70
+ only if a required value is still missing.
71
+
72
+ | Variable | Required | Default |
73
+ |---|---|---|
74
+ | `HGATEWAY_URL` | yes | — |
75
+ | `HGATEWAY_AGENT_API_KEY` | yes | — |
76
+ | `HGATEWAY_REGISTER_ENDPOINT` | no | `/api/v1/sdk/hitl/register` |
77
+ | `HGATEWAY_TIMEOUT_SECONDS` | no | `5` |
78
+
79
+ A missing required value raises `GatewayBootstrapError`.
80
+
81
+ ## Quickstart
82
+
83
+ ```python
84
+ import hgateway_sdk as hg
85
+ from hgateway_sdk import BinaryApprovalContent, Choice, RunFeatures, Routing, Recipients
86
+
87
+ hg.init() # reads HGATEWAY_URL / HGATEWAY_REGISTER_ENDPOINT / HGATEWAY_AGENT_API_KEY (or .env)
88
+
89
+ @hg.hitl_node
90
+ def review_node(state):
91
+ content = BinaryApprovalContent(
92
+ prompt=f"Approve this action? {state['summary']}",
93
+ choices=[Choice("approve", "Approve"), Choice("reject", "Reject")],
94
+ )
95
+ resp = hg.raise_interrupt(
96
+ "approve-action",
97
+ content,
98
+ features=RunFeatures(routing=Routing(primary=Recipients(channels=["#ops"]))),
99
+ fallback={"decision": "reject"}, # surfaced if the gateway is unreachable/declines
100
+ )
101
+ return {"decision": resp["decision"]} # BinaryApprovalResponse is a TypedDict: {"decision": str}
102
+ ```
103
+
104
+ ## Core concepts
105
+
106
+ ### Lifecycle
107
+
108
+ ```
109
+ init() → @hitl_node → raise_interrupt(...) → register + pause → resume
110
+ ```
111
+
112
+ 1. **`init()`** once at startup builds and caches a `GatewayClient` from the env
113
+ (or an injected transport for tests). `raise_interrupt` auto-inits if you skip it.
114
+ 2. **`@hitl_node`** decorates the node. It binds the LangGraph state, config, and an
115
+ occurrence counter into contextvars so the SDK can build the runtime context.
116
+ Calling `raise_interrupt` outside such a node raises `HitlNodeRequired`.
117
+ 3. **`raise_interrupt`** serializes the spec, registers it with the gateway, and
118
+ suspends the graph.
119
+ 4. On **resume** the operator's response is returned as the typed response.
120
+
121
+ ### Ownership vs fallback
122
+
123
+ When the gateway accepts, the SDK suspends with a sentinel value that marks the
124
+ interrupt as gateway-owned:
125
+
126
+ ```python
127
+ {"__hgateway_owned__": True, "hitl_instance_id": "..."}
128
+ ```
129
+
130
+ A backend-for-agent (BFA) inspecting pending interrupts uses this sentinel to know
131
+ the gateway is driving the interaction and should *not* render its own UI. When the
132
+ gateway is unreachable or declines, the SDK instead suspends with your `fallback`
133
+ dict (or the full wire spec if you passed none) — that is your local-handling path.
134
+
135
+ ### Replay-safety
136
+
137
+ LangGraph **re-executes the node from the top** every time the graph resumes, and
138
+ `raise_interrupt` **re-registers on each replay**. Do not place unguarded side
139
+ effects (charging a card, sending an email, mutating external state) *before* the
140
+ interrupt — they will run again on resume. Keep everything before `raise_interrupt`
141
+ pure/idempotent, and perform side effects after the response is in hand.
142
+
143
+ ### JSON-serializability
144
+
145
+ Content fields and the graph `state` are serialized onto the wire. They must be
146
+ JSON-safe (no dataclass instances, sets, datetimes, etc. nested in `state` or
147
+ content values) or registration fails at the transport.
148
+
149
+ ### Interrupt families and response shapes
150
+
151
+ The 4 families map to 10 content types, each paired with a response TypedDict:
152
+
153
+ | Family | Content class | Response shape |
154
+ |---|---|---|
155
+ | Approval | `BinaryApprovalContent` | `{"decision": str}` |
156
+ | Approval | `ModifyApprovalContent` | `{"decision": str, "values"?: dict}` |
157
+ | Decision | `SingleDecisionContent` | `{"selected": str}` |
158
+ | Decision | `MultiDecisionContent` | `{"selected": list[str]}` |
159
+ | Decision | `RankDecisionContent` | `{"ranking": list[str]}` |
160
+ | Context | `FreetextContextContent` | `{"value": str}` |
161
+ | Context | `FormContextContent` | `{"values": dict}` |
162
+ | Context | `ConfirmContextContent` | `{"confirmed": bool}` |
163
+ | Edit | `ContentEditContent` | `{"content": str}` |
164
+ | Edit | `ToolArgsEditContent` | `{"args": dict}` |
165
+
166
+ Responses are `TypedDict`s — access them by key (`resp["decision"]`), never by
167
+ attribute.
168
+
169
+ ## Recipes
170
+
171
+ ### A decision
172
+
173
+ ```python
174
+ import hgateway_sdk as hg
175
+ from hgateway_sdk import SingleDecisionContent, Option
176
+
177
+ resp = hg.raise_interrupt(
178
+ "pick-region",
179
+ SingleDecisionContent(
180
+ prompt="Which region should we deploy to?",
181
+ options=[Option("us", "US"), Option("eu", "EU")],
182
+ ),
183
+ )
184
+ region = resp["selected"]
185
+ ```
186
+
187
+ ### An approval with routing + TTL
188
+
189
+ ```python
190
+ import hgateway_sdk as hg
191
+ from hgateway_sdk import BinaryApprovalContent, RunFeatures, Routing, Recipients, Ttl, OnExpiry
192
+
193
+ features = RunFeatures(
194
+ routing=Routing(
195
+ primary=Recipients(channels=["#ops-approvals"]),
196
+ secondary=Recipients(users=["manager@example.com"]),
197
+ ),
198
+ ttl=Ttl(seconds=1800, on_expiry=OnExpiry.FORWARD_TO_SECONDARY),
199
+ )
200
+ resp = hg.raise_interrupt("deploy-approval", BinaryApprovalContent(prompt="Deploy?"), features=features)
201
+ ```
202
+
203
+ ### The fallback pattern
204
+
205
+ ```python
206
+ resp = hg.raise_interrupt(
207
+ "approve-action",
208
+ BinaryApprovalContent(prompt="Approve?"),
209
+ fallback={"decision": "reject"}, # used when the gateway is unreachable or declines
210
+ )
211
+ ```
212
+
213
+ ### Testing with a fake transport
214
+
215
+ Inject a fake `GatewayTransport` (see `tests/e2e_test_suite/fakes.py`) so tests never hit the
216
+ network:
217
+
218
+ ```python
219
+ import hgateway_sdk as hg
220
+ from tests.e2e_test_suite.fakes import AcceptingTransport
221
+ from tests.e2e_test_suite.helpers import build_single_node_graph, run, resume, new_thread
222
+ from langgraph.types import Command
223
+
224
+ hg.init(transport=AcceptingTransport())
225
+ # build a 1-node graph around your @hitl_node, run() to the interrupt,
226
+ # then resume(graph, {"decision": "approve"}, thread_id).
227
+ ```
228
+
229
+ ## Error handling
230
+
231
+ Every exception subclasses `HGatewayError`, so a single `except HGatewayError` is a
232
+ safe catch-all.
233
+
234
+ | Exception | Cause | Remedy |
235
+ |---|---|---|
236
+ | `GatewayBootstrapError` | A required env var (`HGATEWAY_URL`, `HGATEWAY_REGISTER_ENDPOINT`, `HGATEWAY_AGENT_API_KEY`) is missing. | Set the env vars or provide a `.env`. |
237
+ | `HitlNodeRequired` | `raise_interrupt` called outside an `@hitl_node` node. | Decorate the calling node with `@hg.hitl_node`. |
238
+ | `FeatureValidationError` | A `RunFeatures`/`Routing`/`Ttl`/`Recipients` is misconfigured (e.g. empty `primary`). | Fix the feature config; validation runs at construction. |
239
+ | `GatewayUnreachable` | The HTTP transport could not reach the gateway. | The SDK falls back to a local `interrupt(fallback)`; ensure `fallback` is set and the gateway is reachable. |
240
+
241
+ ## Versioning / source
242
+
243
+ `hgateway_sdk.__version__` reports the installed version. Public API is re-exported
244
+ from the top-level `hgateway_sdk` namespace. Deep per-symbol reference lives in the
245
+ inline docstrings (IDE hover / `pydoc`).
@@ -0,0 +1,222 @@
1
+ # hgateway-sdk
2
+
3
+ **Raise structured human-in-the-loop (HITL) interrupts from inside your LangGraph
4
+ agent and hand the human interaction to the theved.ai HITL Gateway.**
5
+
6
+ A thin client that carries the *ask* (a typed `HitlContent`) plus SDK-derived run
7
+ context, registers it out-of-band with the gateway, and wraps LangGraph's
8
+ `interrupt()` so the resumed value comes back as the paired typed response.
9
+
10
+ ## What it does / when to use it
11
+
12
+ Use it whenever a LangGraph node needs a human to approve, decide, edit, or supply
13
+ context before the graph continues.
14
+
15
+ The mental model is **ownership vs fallback**:
16
+
17
+ - When the gateway **accepts** an interrupt, it *owns* the human interaction —
18
+ routing, notification, TTL/escalation, and collecting the response. Your graph
19
+ simply suspends and later resumes with the operator's answer.
20
+ - When the gateway is **unreachable or declines**, the SDK falls back to a plain
21
+ local `interrupt(fallback)` so your own backend (or test harness) can resolve it.
22
+
23
+ This lets you adopt the gateway without giving up a working local path.
24
+
25
+ ## Install
26
+
27
+ The SDK is distributed from its (private) GitHub repository — there is no PyPI
28
+ release. Install it pinned to a release tag:
29
+
30
+ ```bash
31
+ uv add "hgateway-sdk @ git+https://github.com/theved-ai/hgateway_sdk.git@v0.1.0"
32
+ # or with pip:
33
+ pip install "hgateway-sdk @ git+https://github.com/theved-ai/hgateway_sdk.git@v0.1.0"
34
+ ```
35
+
36
+ Because the repo is private, the installing environment must be authenticated to
37
+ GitHub (an SSH key, or a token via `GH_TOKEN` / a credential helper / a
38
+ `git+https://<token>@github.com/...` URL).
39
+
40
+ Each release tag also has a built wheel + sdist attached as assets on its
41
+ [GitHub Release](https://github.com/theved-ai/hgateway_sdk/releases); you may
42
+ install directly from a downloaded wheel instead of building from source.
43
+
44
+ ## Configure
45
+
46
+ Settings are resolved from the process environment first; a `.env` file is consulted
47
+ only if a required value is still missing.
48
+
49
+ | Variable | Required | Default |
50
+ |---|---|---|
51
+ | `HGATEWAY_URL` | yes | — |
52
+ | `HGATEWAY_AGENT_API_KEY` | yes | — |
53
+ | `HGATEWAY_REGISTER_ENDPOINT` | no | `/api/v1/sdk/hitl/register` |
54
+ | `HGATEWAY_TIMEOUT_SECONDS` | no | `5` |
55
+
56
+ A missing required value raises `GatewayBootstrapError`.
57
+
58
+ ## Quickstart
59
+
60
+ ```python
61
+ import hgateway_sdk as hg
62
+ from hgateway_sdk import BinaryApprovalContent, Choice, RunFeatures, Routing, Recipients
63
+
64
+ hg.init() # reads HGATEWAY_URL / HGATEWAY_REGISTER_ENDPOINT / HGATEWAY_AGENT_API_KEY (or .env)
65
+
66
+ @hg.hitl_node
67
+ def review_node(state):
68
+ content = BinaryApprovalContent(
69
+ prompt=f"Approve this action? {state['summary']}",
70
+ choices=[Choice("approve", "Approve"), Choice("reject", "Reject")],
71
+ )
72
+ resp = hg.raise_interrupt(
73
+ "approve-action",
74
+ content,
75
+ features=RunFeatures(routing=Routing(primary=Recipients(channels=["#ops"]))),
76
+ fallback={"decision": "reject"}, # surfaced if the gateway is unreachable/declines
77
+ )
78
+ return {"decision": resp["decision"]} # BinaryApprovalResponse is a TypedDict: {"decision": str}
79
+ ```
80
+
81
+ ## Core concepts
82
+
83
+ ### Lifecycle
84
+
85
+ ```
86
+ init() → @hitl_node → raise_interrupt(...) → register + pause → resume
87
+ ```
88
+
89
+ 1. **`init()`** once at startup builds and caches a `GatewayClient` from the env
90
+ (or an injected transport for tests). `raise_interrupt` auto-inits if you skip it.
91
+ 2. **`@hitl_node`** decorates the node. It binds the LangGraph state, config, and an
92
+ occurrence counter into contextvars so the SDK can build the runtime context.
93
+ Calling `raise_interrupt` outside such a node raises `HitlNodeRequired`.
94
+ 3. **`raise_interrupt`** serializes the spec, registers it with the gateway, and
95
+ suspends the graph.
96
+ 4. On **resume** the operator's response is returned as the typed response.
97
+
98
+ ### Ownership vs fallback
99
+
100
+ When the gateway accepts, the SDK suspends with a sentinel value that marks the
101
+ interrupt as gateway-owned:
102
+
103
+ ```python
104
+ {"__hgateway_owned__": True, "hitl_instance_id": "..."}
105
+ ```
106
+
107
+ A backend-for-agent (BFA) inspecting pending interrupts uses this sentinel to know
108
+ the gateway is driving the interaction and should *not* render its own UI. When the
109
+ gateway is unreachable or declines, the SDK instead suspends with your `fallback`
110
+ dict (or the full wire spec if you passed none) — that is your local-handling path.
111
+
112
+ ### Replay-safety
113
+
114
+ LangGraph **re-executes the node from the top** every time the graph resumes, and
115
+ `raise_interrupt` **re-registers on each replay**. Do not place unguarded side
116
+ effects (charging a card, sending an email, mutating external state) *before* the
117
+ interrupt — they will run again on resume. Keep everything before `raise_interrupt`
118
+ pure/idempotent, and perform side effects after the response is in hand.
119
+
120
+ ### JSON-serializability
121
+
122
+ Content fields and the graph `state` are serialized onto the wire. They must be
123
+ JSON-safe (no dataclass instances, sets, datetimes, etc. nested in `state` or
124
+ content values) or registration fails at the transport.
125
+
126
+ ### Interrupt families and response shapes
127
+
128
+ The 4 families map to 10 content types, each paired with a response TypedDict:
129
+
130
+ | Family | Content class | Response shape |
131
+ |---|---|---|
132
+ | Approval | `BinaryApprovalContent` | `{"decision": str}` |
133
+ | Approval | `ModifyApprovalContent` | `{"decision": str, "values"?: dict}` |
134
+ | Decision | `SingleDecisionContent` | `{"selected": str}` |
135
+ | Decision | `MultiDecisionContent` | `{"selected": list[str]}` |
136
+ | Decision | `RankDecisionContent` | `{"ranking": list[str]}` |
137
+ | Context | `FreetextContextContent` | `{"value": str}` |
138
+ | Context | `FormContextContent` | `{"values": dict}` |
139
+ | Context | `ConfirmContextContent` | `{"confirmed": bool}` |
140
+ | Edit | `ContentEditContent` | `{"content": str}` |
141
+ | Edit | `ToolArgsEditContent` | `{"args": dict}` |
142
+
143
+ Responses are `TypedDict`s — access them by key (`resp["decision"]`), never by
144
+ attribute.
145
+
146
+ ## Recipes
147
+
148
+ ### A decision
149
+
150
+ ```python
151
+ import hgateway_sdk as hg
152
+ from hgateway_sdk import SingleDecisionContent, Option
153
+
154
+ resp = hg.raise_interrupt(
155
+ "pick-region",
156
+ SingleDecisionContent(
157
+ prompt="Which region should we deploy to?",
158
+ options=[Option("us", "US"), Option("eu", "EU")],
159
+ ),
160
+ )
161
+ region = resp["selected"]
162
+ ```
163
+
164
+ ### An approval with routing + TTL
165
+
166
+ ```python
167
+ import hgateway_sdk as hg
168
+ from hgateway_sdk import BinaryApprovalContent, RunFeatures, Routing, Recipients, Ttl, OnExpiry
169
+
170
+ features = RunFeatures(
171
+ routing=Routing(
172
+ primary=Recipients(channels=["#ops-approvals"]),
173
+ secondary=Recipients(users=["manager@example.com"]),
174
+ ),
175
+ ttl=Ttl(seconds=1800, on_expiry=OnExpiry.FORWARD_TO_SECONDARY),
176
+ )
177
+ resp = hg.raise_interrupt("deploy-approval", BinaryApprovalContent(prompt="Deploy?"), features=features)
178
+ ```
179
+
180
+ ### The fallback pattern
181
+
182
+ ```python
183
+ resp = hg.raise_interrupt(
184
+ "approve-action",
185
+ BinaryApprovalContent(prompt="Approve?"),
186
+ fallback={"decision": "reject"}, # used when the gateway is unreachable or declines
187
+ )
188
+ ```
189
+
190
+ ### Testing with a fake transport
191
+
192
+ Inject a fake `GatewayTransport` (see `tests/e2e_test_suite/fakes.py`) so tests never hit the
193
+ network:
194
+
195
+ ```python
196
+ import hgateway_sdk as hg
197
+ from tests.e2e_test_suite.fakes import AcceptingTransport
198
+ from tests.e2e_test_suite.helpers import build_single_node_graph, run, resume, new_thread
199
+ from langgraph.types import Command
200
+
201
+ hg.init(transport=AcceptingTransport())
202
+ # build a 1-node graph around your @hitl_node, run() to the interrupt,
203
+ # then resume(graph, {"decision": "approve"}, thread_id).
204
+ ```
205
+
206
+ ## Error handling
207
+
208
+ Every exception subclasses `HGatewayError`, so a single `except HGatewayError` is a
209
+ safe catch-all.
210
+
211
+ | Exception | Cause | Remedy |
212
+ |---|---|---|
213
+ | `GatewayBootstrapError` | A required env var (`HGATEWAY_URL`, `HGATEWAY_REGISTER_ENDPOINT`, `HGATEWAY_AGENT_API_KEY`) is missing. | Set the env vars or provide a `.env`. |
214
+ | `HitlNodeRequired` | `raise_interrupt` called outside an `@hitl_node` node. | Decorate the calling node with `@hg.hitl_node`. |
215
+ | `FeatureValidationError` | A `RunFeatures`/`Routing`/`Ttl`/`Recipients` is misconfigured (e.g. empty `primary`). | Fix the feature config; validation runs at construction. |
216
+ | `GatewayUnreachable` | The HTTP transport could not reach the gateway. | The SDK falls back to a local `interrupt(fallback)`; ensure `fallback` is set and the gateway is reachable. |
217
+
218
+ ## Versioning / source
219
+
220
+ `hgateway_sdk.__version__` reports the installed version. Public API is re-exported
221
+ from the top-level `hgateway_sdk` namespace. Deep per-symbol reference lives in the
222
+ inline docstrings (IDE hover / `pydoc`).
@@ -0,0 +1,54 @@
1
+ [project]
2
+ name = "hgateway-sdk"
3
+ version = "0.1.0"
4
+ description = "theved.ai HITL Gateway SDK — raise human-in-the-loop interrupts from LangGraph nodes."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ authors = [{name = "theved.ai"}]
8
+ classifiers = [
9
+ "Development Status :: 4 - Beta",
10
+ "Intended Audience :: Developers",
11
+ "Programming Language :: Python :: 3",
12
+ "Programming Language :: Python :: 3.10",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Topic :: Software Development :: Libraries :: Python Modules",
17
+ ]
18
+ dependencies = [
19
+ "langgraph>=1.0",
20
+ "langchain-core>=1.0",
21
+ "python-dotenv>=1.2.2",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ dev = ["pytest>=8"]
26
+
27
+ [dependency-groups]
28
+ dev = ["pytest>=8", "pytest-cov>=5"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://theved.ai"
32
+ Source = "https://github.com/theved-ai/hgateway_sdk"
33
+
34
+ [build-system]
35
+ requires = ["hatchling"]
36
+ build-backend = "hatchling.build"
37
+
38
+ # Build-time ignore list (the hatchling equivalent of a ".buildignore").
39
+ # Applies to every build target — only SDK code ships to developers; tests,
40
+ # local env files, and caches are never included in the wheel or sdist.
41
+ [tool.hatch.build]
42
+ exclude = [
43
+ "tests/",
44
+ "*.env",
45
+ ".env*",
46
+ "**/__pycache__",
47
+ "**/*.py[cod]",
48
+ ]
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["src/hgateway_sdk"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests/e2e_test_suite"]