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.
- hgateway_sdk-0.1.0/.github/workflows/publish-pypi.yml +28 -0
- hgateway_sdk-0.1.0/.github/workflows/release.yml +47 -0
- hgateway_sdk-0.1.0/.gitignore +21 -0
- hgateway_sdk-0.1.0/PKG-INFO +245 -0
- hgateway_sdk-0.1.0/README.md +222 -0
- hgateway_sdk-0.1.0/pyproject.toml +54 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/__init__.py +177 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/client/__init__.py +12 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/client/entrypoint.py +140 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/client/gateway_client.py +128 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/config/__init__.py +11 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/config/loader.py +79 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/constants/__init__.py +39 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/constants/field_type.py +34 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/constants/hitl_sub_type.py +78 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/constants/hitl_type.py +36 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/constants/on_expiry.py +28 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/constants/values.py +178 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/decorators.py +87 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/exceptions/__init__.py +35 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/exceptions/feature_validation_error.py +29 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/exceptions/gateway_bootstrap_error.py +27 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/exceptions/gateway_unreachable.py +31 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/exceptions/hgateway_error.py +16 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/exceptions/hitl_node_required.py +32 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/py.typed +0 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/runtime/__init__.py +19 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/runtime/context.py +142 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/__init__.py +117 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/ack.py +35 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/__init__.py +58 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/approval_content.py +31 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/binary_approval_content.py +55 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/confirm_context_content.py +46 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/content_edit_content.py +46 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/context_content.py +32 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/decision_content.py +32 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/edit_content.py +31 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/form_context_content.py +48 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/freetext_context_content.py +45 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/hitl_content.py +42 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/modify_approval_content.py +53 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/multi_decision_content.py +56 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/rank_decision_content.py +47 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/single_decision_content.py +45 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/content/tool_args_edit_content.py +52 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/__init__.py +28 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/choice.py +42 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/form_field.py +49 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/freetext_input.py +35 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/modifiable_field.py +52 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/option.py +45 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/elements/tool_arg.py +49 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/envelope/__init__.py +11 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/envelope/hitl_spec.py +54 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/envelope/runtime_context.py +43 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/__init__.py +39 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/cross_feature_rule.py +15 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/feature.py +16 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/recipients.py +39 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/routing.py +46 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/run_features.py +54 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/ttl.py +65 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/ttl_forward_needs_secondary.py +27 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/features/validate_features.py +47 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/__init__.py +46 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/binary_approval_response.py +19 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/confirm_context_response.py +18 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/content_edit_response.py +20 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/form_context_response.py +19 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/freetext_context_response.py +17 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/modify_approval_response.py +23 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/multi_decision_response.py +19 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/rank_decision_response.py +19 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/single_decision_response.py +17 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/responses/tool_args_edit_response.py +21 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/schemas/settings.py +30 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/transport/__init__.py +15 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/transport/gateway_transport.py +54 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/transport/http_gateway_transport.py +135 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/utils/__init__.py +10 -0
- hgateway_sdk-0.1.0/src/hgateway_sdk/utils/serialization.py +99 -0
- 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"]
|