openbox-langgraph-sdk-python 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.
- openbox_langgraph_sdk_python-0.1.0/.github/workflows/publish.yml +71 -0
- openbox_langgraph_sdk_python-0.1.0/.gitignore +2 -0
- openbox_langgraph_sdk_python-0.1.0/PKG-INFO +492 -0
- openbox_langgraph_sdk_python-0.1.0/README.md +455 -0
- openbox_langgraph_sdk_python-0.1.0/docs/code-standards.md +523 -0
- openbox_langgraph_sdk_python-0.1.0/docs/codebase-summary.md +308 -0
- openbox_langgraph_sdk_python-0.1.0/docs/project-overview-pdr.md +187 -0
- openbox_langgraph_sdk_python-0.1.0/docs/project-roadmap.md +327 -0
- openbox_langgraph_sdk_python-0.1.0/docs/system-architecture.md +603 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/__init__.py +130 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/client.py +358 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/config.py +264 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/db_governance_hooks.py +897 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/errors.py +114 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/file_governance_hooks.py +413 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/hitl.py +88 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/hook_governance.py +397 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/http_governance_hooks.py +695 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/langgraph_handler.py +1616 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/otel_setup.py +468 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/span_processor.py +253 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/tracing.py +352 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/types.py +485 -0
- openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/verdict_handler.py +203 -0
- openbox_langgraph_sdk_python-0.1.0/pyproject.toml +63 -0
- openbox_langgraph_sdk_python-0.1.0/test-agent/.env +11 -0
- openbox_langgraph_sdk_python-0.1.0/test-agent/.env.example +9 -0
- openbox_langgraph_sdk_python-0.1.0/test-agent/README.md +27 -0
- openbox_langgraph_sdk_python-0.1.0/test-agent/SETUP.md +455 -0
- openbox_langgraph_sdk_python-0.1.0/test-agent/agent.py +172 -0
- openbox_langgraph_sdk_python-0.1.0/test-agent/pyproject.toml +16 -0
- openbox_langgraph_sdk_python-0.1.0/test-agent/uv.lock +1218 -0
- openbox_langgraph_sdk_python-0.1.0/tests/test_contextvars_propagation.py +59 -0
- openbox_langgraph_sdk_python-0.1.0/tests/test_governance_changes.py +279 -0
- openbox_langgraph_sdk_python-0.1.0/tests/test_telemetry_payload.py +333 -0
- openbox_langgraph_sdk_python-0.1.0/uv.lock +1477 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["*.*.*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.11"
|
|
16
|
+
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v4
|
|
19
|
+
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: uv sync --all-extras
|
|
22
|
+
|
|
23
|
+
- name: Lint
|
|
24
|
+
run: uv run ruff check openbox_langgraph/
|
|
25
|
+
|
|
26
|
+
- name: Test
|
|
27
|
+
run: uv run pytest
|
|
28
|
+
|
|
29
|
+
- name: Verify tag matches package version
|
|
30
|
+
run: |
|
|
31
|
+
TAG="${GITHUB_REF#refs/tags/}"
|
|
32
|
+
PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
33
|
+
if [ "$TAG" != "$PKG_VERSION" ]; then
|
|
34
|
+
echo "::error::Tag $TAG does not match pyproject.toml version $PKG_VERSION"
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
build:
|
|
39
|
+
needs: test
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- uses: actions/setup-python@v5
|
|
45
|
+
with:
|
|
46
|
+
python-version: "3.11"
|
|
47
|
+
|
|
48
|
+
- name: Build sdist and wheel
|
|
49
|
+
run: pip install build && python -m build
|
|
50
|
+
|
|
51
|
+
- uses: actions/upload-artifact@v4
|
|
52
|
+
with:
|
|
53
|
+
name: dist
|
|
54
|
+
path: dist/
|
|
55
|
+
|
|
56
|
+
publish:
|
|
57
|
+
needs: build
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
environment: pypi
|
|
60
|
+
permissions:
|
|
61
|
+
id-token: write
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/download-artifact@v4
|
|
64
|
+
with:
|
|
65
|
+
name: dist
|
|
66
|
+
path: dist/
|
|
67
|
+
|
|
68
|
+
- name: Publish to PyPI
|
|
69
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
70
|
+
with:
|
|
71
|
+
verbose: true
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openbox-langgraph-sdk-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenBox governance and observability SDK for LangGraph
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: httpx>=0.27.0
|
|
8
|
+
Requires-Dist: langchain-core>=0.3.0
|
|
9
|
+
Requires-Dist: langgraph>=0.2.0
|
|
10
|
+
Requires-Dist: opentelemetry-api>=1.20.0
|
|
11
|
+
Requires-Dist: opentelemetry-instrumentation-asyncpg>=0.41b0
|
|
12
|
+
Requires-Dist: opentelemetry-instrumentation-httpx>=0.41b0
|
|
13
|
+
Requires-Dist: opentelemetry-instrumentation-mysql>=0.41b0
|
|
14
|
+
Requires-Dist: opentelemetry-instrumentation-psycopg2>=0.41b0
|
|
15
|
+
Requires-Dist: opentelemetry-instrumentation-pymongo>=0.41b0
|
|
16
|
+
Requires-Dist: opentelemetry-instrumentation-pymysql>=0.41b0
|
|
17
|
+
Requires-Dist: opentelemetry-instrumentation-redis>=0.41b0
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation-requests>=0.41b0
|
|
19
|
+
Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.41b0
|
|
20
|
+
Requires-Dist: opentelemetry-instrumentation-sqlite3>=0.41b0
|
|
21
|
+
Requires-Dist: opentelemetry-instrumentation-urllib3>=0.41b0
|
|
22
|
+
Requires-Dist: opentelemetry-instrumentation-urllib>=0.41b0
|
|
23
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
29
|
+
Provides-Extra: otel
|
|
30
|
+
Requires-Dist: opentelemetry-api>=1.20.0; extra == 'otel'
|
|
31
|
+
Requires-Dist: opentelemetry-instrumentation-httpx>=0.41b0; extra == 'otel'
|
|
32
|
+
Requires-Dist: opentelemetry-instrumentation-requests>=0.41b0; extra == 'otel'
|
|
33
|
+
Requires-Dist: opentelemetry-instrumentation-urllib3>=0.41b0; extra == 'otel'
|
|
34
|
+
Requires-Dist: opentelemetry-instrumentation-urllib>=0.41b0; extra == 'otel'
|
|
35
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'otel'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# openbox-langgraph-sdk
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/openbox-langgraph-sdk/)
|
|
41
|
+
[](https://pypi.org/project/openbox-langgraph-sdk/)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
|
|
44
|
+
Real-time governance and observability for [LangGraph](https://github.com/langchain-ai/langgraph) agents — powered by [OpenBox](https://openbox.ai).
|
|
45
|
+
|
|
46
|
+
**OpenBox** sits between your agent and the world. Every tool call, LLM prompt, and subagent dispatch passes through a policy engine before it executes. You write policies in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/); OpenBox enforces them — blocking harmful actions, screening for PII, and routing sensitive operations to a human approver — all without changing your agent code.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Table of Contents
|
|
51
|
+
|
|
52
|
+
- [How it works](#how-it-works)
|
|
53
|
+
- [Installation](#installation)
|
|
54
|
+
- [Quickstart](#quickstart)
|
|
55
|
+
- [Configuration reference](#configuration-reference)
|
|
56
|
+
- [Governance features](#governance-features)
|
|
57
|
+
- [Policies (OPA / Rego)](#policies-opa--rego)
|
|
58
|
+
- [Guardrails](#guardrails)
|
|
59
|
+
- [Human-in-the-loop (HITL)](#human-in-the-loop-hitl)
|
|
60
|
+
- [Behavior Rules (AGE)](#behavior-rules-age)
|
|
61
|
+
- [Tool classification](#tool-classification)
|
|
62
|
+
- [Error handling](#error-handling)
|
|
63
|
+
- [Advanced usage](#advanced-usage)
|
|
64
|
+
- [Debugging](#debugging)
|
|
65
|
+
- [Contributing](#contributing)
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## How it works
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
Your code SDK OpenBox Core
|
|
73
|
+
────────── ─── ────────────
|
|
74
|
+
governed.ainvoke() → streams LangGraph events → Policy engine (OPA / Rego)
|
|
75
|
+
on_tool_start → Guardrails (PII, toxicity…)
|
|
76
|
+
on_chat_model_start → Behavior Rules (AGE)
|
|
77
|
+
on_chain_start → HITL approval queue
|
|
78
|
+
↑
|
|
79
|
+
enforce verdict
|
|
80
|
+
(allow / block / redact / pause)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The SDK wraps your compiled LangGraph graph and intercepts every event in the [LangGraph v2 event stream](https://langchain-ai.github.io/langgraph/how-tos/streaming-events-from-within-tools/). It sends governance events to OpenBox Core, receives verdicts, and enforces them — all before your tool or LLM call continues.
|
|
84
|
+
|
|
85
|
+
**Zero graph changes required.** You keep writing LangGraph exactly as you normally would.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Installation
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install openbox-langgraph-sdk
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Requirements:** Python 3.11+, `langgraph >= 0.2`, `langchain-core >= 0.2`
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Quickstart
|
|
100
|
+
|
|
101
|
+
### 1. Get your API key
|
|
102
|
+
|
|
103
|
+
Sign in to [dashboard.openbox.ai](https://dashboard.openbox.ai), create an agent called `"MyAgent"`, and copy your API key (`obx_live_...` or `obx_test_...`).
|
|
104
|
+
|
|
105
|
+
### 2. Set environment variables
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
export OPENBOX_URL="https://core.openbox.ai"
|
|
109
|
+
export OPENBOX_API_KEY="obx_live_..."
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 3. Wrap your graph
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import os
|
|
116
|
+
import asyncio
|
|
117
|
+
from langgraph.prebuilt import create_react_agent
|
|
118
|
+
from langchain_openai import ChatOpenAI
|
|
119
|
+
from openbox_langgraph import create_openbox_graph_handler
|
|
120
|
+
|
|
121
|
+
# Your existing agent — no changes needed
|
|
122
|
+
llm = ChatOpenAI(model="gpt-4o-mini")
|
|
123
|
+
agent = create_react_agent(llm, tools=[search_web, write_file])
|
|
124
|
+
|
|
125
|
+
async def main():
|
|
126
|
+
governed = await create_openbox_graph_handler(
|
|
127
|
+
graph=agent,
|
|
128
|
+
api_url=os.environ["OPENBOX_URL"],
|
|
129
|
+
api_key=os.environ["OPENBOX_API_KEY"],
|
|
130
|
+
agent_name="MyAgent", # must match the agent name in your dashboard
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
result = await governed.ainvoke(
|
|
134
|
+
{"messages": [{"role": "user", "content": "Search for the latest AI papers"}]},
|
|
135
|
+
config={"configurable": {"thread_id": "session-001"}},
|
|
136
|
+
)
|
|
137
|
+
print(result["messages"][-1].content)
|
|
138
|
+
|
|
139
|
+
asyncio.run(main())
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
That's it. Your agent now sends governance events to OpenBox on every tool call and LLM prompt.
|
|
143
|
+
|
|
144
|
+
### Try it locally (included test agent)
|
|
145
|
+
|
|
146
|
+
This repository includes a runnable LangGraph test agent under `sdk-langgraph-python/test-agent`.
|
|
147
|
+
|
|
148
|
+
It is designed to validate:
|
|
149
|
+
|
|
150
|
+
- **Guardrails** on `agent_validatePrompt`
|
|
151
|
+
- **Policies** on tool invocations (BLOCK / REQUIRE_APPROVAL)
|
|
152
|
+
- **HITL** approval polling
|
|
153
|
+
- **Behavior Rules (AGE)** via `httpx` spans from `search_web`
|
|
154
|
+
|
|
155
|
+
See `test-agent/README.md` for setup and run instructions.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Configuration reference
|
|
160
|
+
|
|
161
|
+
`create_openbox_graph_handler` accepts the following keyword arguments:
|
|
162
|
+
|
|
163
|
+
| Parameter | Type | Default | Description |
|
|
164
|
+
|---|---|---|---|
|
|
165
|
+
| `graph` | `CompiledGraph` | **required** | Your compiled LangGraph graph |
|
|
166
|
+
| `api_url` | `str` | **required** | Base URL of your OpenBox Core instance |
|
|
167
|
+
| `api_key` | `str` | **required** | API key (`obx_live_*` or `obx_test_*`) |
|
|
168
|
+
| `agent_name` | `str` | `None` | Agent name as configured in the dashboard (used for policy lookup and execution tree) |
|
|
169
|
+
| `validate` | `bool` | `True` | Validate API key against server on startup |
|
|
170
|
+
| `on_api_error` | `str` | `"fail_open"` | `"fail_open"` (allow on error) or `"fail_closed"` (block on error) |
|
|
171
|
+
| `governance_timeout` | `float` | `30.0` | HTTP timeout in seconds for governance calls |
|
|
172
|
+
| `session_id` | `str` | `None` | Optional session identifier for multi-session agents |
|
|
173
|
+
| `task_queue` | `str` | `"langgraph"` | Task queue label attached to all governance events |
|
|
174
|
+
| `hitl` | `dict` | `{}` | Human-in-the-loop config (see [HITL](#human-in-the-loop-hitl)) |
|
|
175
|
+
| `tool_type_map` | `dict[str, str]` | `{}` | Map tool names to semantic types for policy classification (see [Tool classification](#tool-classification)) |
|
|
176
|
+
| `skip_chain_types` | `set[str]` | `set()` | Chain node names to skip (no governance event sent) |
|
|
177
|
+
| `skip_tool_types` | `set[str]` | `set()` | Tool names to skip entirely |
|
|
178
|
+
| `send_chain_start_event` | `bool` | `True` | Send `WorkflowStarted` event |
|
|
179
|
+
| `send_chain_end_event` | `bool` | `True` | Send `WorkflowCompleted` event |
|
|
180
|
+
| `send_llm_start_event` | `bool` | `True` | Send `LLMStarted` event (enables prompt guardrails) |
|
|
181
|
+
| `send_llm_end_event` | `bool` | `True` | Send `LLMCompleted` event |
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Governance features
|
|
186
|
+
|
|
187
|
+
### Policies (OPA / Rego)
|
|
188
|
+
|
|
189
|
+
Policies are written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) and configured in the OpenBox dashboard under your agent. The SDK sends an `ActivityStarted` event before every tool call; your policy decides what happens next.
|
|
190
|
+
|
|
191
|
+
**Fields available in `input`:**
|
|
192
|
+
|
|
193
|
+
| Field | Type | Description |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `input.event_type` | `string` | `"ActivityStarted"` or `"ActivityCompleted"` |
|
|
196
|
+
| `input.activity_type` | `string` | Tool name (e.g. `"search_web"`) |
|
|
197
|
+
| `input.activity_input` | `array` | Tool arguments as a JSON array |
|
|
198
|
+
| `input.workflow_type` | `string` | Your `agent_name` |
|
|
199
|
+
| `input.workflow_id` | `string` | Session workflow ID |
|
|
200
|
+
| `input.trust_tier` | `int` | Agent trust tier (1–4) from dashboard |
|
|
201
|
+
| `input.hook_trigger` | `bool` | `true` when event is a hook-level re-evaluation (see [below](#the-hook_trigger-guard)) |
|
|
202
|
+
|
|
203
|
+
**Example — block a restricted search term:**
|
|
204
|
+
|
|
205
|
+
```rego
|
|
206
|
+
package org.openboxai.policy
|
|
207
|
+
|
|
208
|
+
import future.keywords.if
|
|
209
|
+
import future.keywords.in
|
|
210
|
+
|
|
211
|
+
default result = {"decision": "CONTINUE", "reason": null}
|
|
212
|
+
|
|
213
|
+
restricted_terms := {"nuclear weapon", "bioweapon", "malware synthesis"}
|
|
214
|
+
|
|
215
|
+
result := {"decision": "BLOCK", "reason": "Restricted topic."} if {
|
|
216
|
+
input.event_type == "ActivityStarted"
|
|
217
|
+
input.activity_type == "search_web"
|
|
218
|
+
not input.hook_trigger
|
|
219
|
+
count(input.activity_input) > 0
|
|
220
|
+
entry := input.activity_input[0]
|
|
221
|
+
is_object(entry)
|
|
222
|
+
some term in restricted_terms
|
|
223
|
+
contains(lower(entry.query), term)
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Example — require approval for sensitive exports:**
|
|
228
|
+
|
|
229
|
+
```rego
|
|
230
|
+
result := {"decision": "REQUIRE_APPROVAL", "reason": "Data export requires compliance sign-off."} if {
|
|
231
|
+
input.event_type == "ActivityStarted"
|
|
232
|
+
input.activity_type == "export_data"
|
|
233
|
+
not input.hook_trigger
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Possible decisions:**
|
|
238
|
+
|
|
239
|
+
| Decision | Effect |
|
|
240
|
+
|---|---|
|
|
241
|
+
| `CONTINUE` | Tool executes normally |
|
|
242
|
+
| `BLOCK` | `GovernanceBlockedError` raised — tool does not execute |
|
|
243
|
+
| `REQUIRE_APPROVAL` | Agent pauses; human must approve or reject in dashboard |
|
|
244
|
+
| `HALT` | `GovernanceHaltError` raised — session terminated |
|
|
245
|
+
|
|
246
|
+
#### The `hook_trigger` guard
|
|
247
|
+
|
|
248
|
+
The SDK's HTTP telemetry layer intercepts outgoing HTTP requests made by your tools and sends a second `ActivityStarted` event for the underlying network call. This event has `hook_trigger: true`.
|
|
249
|
+
|
|
250
|
+
**Always add `not input.hook_trigger`** to `BLOCK` and `REQUIRE_APPROVAL` rules to prevent them from double-firing on the HTTP-level re-evaluation.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### Guardrails
|
|
255
|
+
|
|
256
|
+
Guardrails screen the content of LLM prompts and tool outputs. Configure them in the dashboard per agent.
|
|
257
|
+
|
|
258
|
+
Supported guardrail types:
|
|
259
|
+
|
|
260
|
+
| Type | ID | What it detects |
|
|
261
|
+
|---|---|---|
|
|
262
|
+
| PII detection | `1` | Names, emails, phone numbers, SSNs, credit cards |
|
|
263
|
+
| Content filter | `2` | Harmful or unsafe content categories |
|
|
264
|
+
| Toxicity | `3` | Toxic language |
|
|
265
|
+
| Ban words | `4` | Custom word/phrase blocklist |
|
|
266
|
+
| Regex | `5` | Custom regex patterns |
|
|
267
|
+
|
|
268
|
+
When a guardrail fires on an LLM prompt:
|
|
269
|
+
- **PII redaction** — the prompt is automatically redacted before the LLM sees it
|
|
270
|
+
- **Content block** — `GuardrailsValidationError` is raised
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
### Human-in-the-loop (HITL)
|
|
275
|
+
|
|
276
|
+
When a policy returns `REQUIRE_APPROVAL`, the agent pauses and polls OpenBox for a human decision. Enable HITL in the handler:
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
governed = await create_openbox_graph_handler(
|
|
280
|
+
graph=agent,
|
|
281
|
+
api_url=os.environ["OPENBOX_URL"],
|
|
282
|
+
api_key=os.environ["OPENBOX_API_KEY"],
|
|
283
|
+
agent_name="MyAgent",
|
|
284
|
+
hitl={
|
|
285
|
+
"enabled": True,
|
|
286
|
+
"poll_interval_ms": 5_000, # check every 5 seconds
|
|
287
|
+
"max_wait_ms": 300_000, # timeout after 5 minutes
|
|
288
|
+
},
|
|
289
|
+
)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The human approves or rejects from the OpenBox dashboard. The SDK resumes or raises `ApprovalRejectedError` accordingly.
|
|
293
|
+
|
|
294
|
+
**HITL config options:**
|
|
295
|
+
|
|
296
|
+
| Key | Type | Default | Description |
|
|
297
|
+
|---|---|---|---|
|
|
298
|
+
| `enabled` | `bool` | `False` | Enable HITL polling |
|
|
299
|
+
| `poll_interval_ms` | `int` | `5000` | How often to poll for a decision |
|
|
300
|
+
| `max_wait_ms` | `int` | `300000` | Total timeout before `ApprovalTimeoutError` |
|
|
301
|
+
| `skip_tool_types` | `set[str]` | `set()` | Tools that never wait for HITL |
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
### Behavior Rules (AGE)
|
|
306
|
+
|
|
307
|
+
Behavior Rules detect patterns across sequences of tool calls within a session. They are configured in the dashboard and enforced by the OpenBox Activity Governance Engine (AGE).
|
|
308
|
+
|
|
309
|
+
Example use cases:
|
|
310
|
+
- Flag if an agent calls an external URL more than N times in one session
|
|
311
|
+
- Detect unusual tool call sequences (e.g. data exfiltration patterns)
|
|
312
|
+
- Enforce rate limits per tool type
|
|
313
|
+
|
|
314
|
+
The SDK automatically attaches HTTP span telemetry (via `httpx` hooks) so that `http_get` / `http_post` spans are captured and sent with `ActivityCompleted` events.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### Tool classification
|
|
319
|
+
|
|
320
|
+
The SDK can classify tools into semantic types, which enriches the execution tree in the dashboard and enables type-based policy matching via the `__openbox` metadata mechanism (described below).
|
|
321
|
+
|
|
322
|
+
Configure `tool_type_map` when creating the handler:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
governed = await create_openbox_graph_handler(
|
|
326
|
+
graph=agent,
|
|
327
|
+
api_url=os.environ["OPENBOX_URL"],
|
|
328
|
+
api_key=os.environ["OPENBOX_API_KEY"],
|
|
329
|
+
agent_name="MyAgent",
|
|
330
|
+
tool_type_map={
|
|
331
|
+
"search_web": "http",
|
|
332
|
+
"export_data": "http",
|
|
333
|
+
"query_db": "database",
|
|
334
|
+
"write_file": "builtin",
|
|
335
|
+
},
|
|
336
|
+
)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Supported `tool_type` values:** `"http"`, `"database"`, `"builtin"`, `"a2a"`
|
|
340
|
+
|
|
341
|
+
#### Matching on tool type in Rego
|
|
342
|
+
|
|
343
|
+
When a tool type is resolved, the SDK appends an `__openbox` metadata sentinel to `activity_input`. This lets Rego policies classify tools without requiring any changes to OpenBox Core:
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
"activity_input": [
|
|
347
|
+
{"query": "latest AI papers"},
|
|
348
|
+
{"__openbox": {"tool_type": "http"}}
|
|
349
|
+
]
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Rego can then match on it:
|
|
353
|
+
|
|
354
|
+
```rego
|
|
355
|
+
result := {"decision": "REQUIRE_APPROVAL", "reason": "HTTP calls require approval."} if {
|
|
356
|
+
input.event_type == "ActivityStarted"
|
|
357
|
+
not input.hook_trigger
|
|
358
|
+
some item in input.activity_input
|
|
359
|
+
item["__openbox"].tool_type == "http"
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
> **Tip:** `activity_type` (the tool name) is always the most reliable field to match on for specific tools. Use `__openbox.tool_type` when you want to write rules that apply to an entire category of tools.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Error handling
|
|
368
|
+
|
|
369
|
+
The SDK raises typed exceptions that you can catch:
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
from openbox_langgraph import (
|
|
373
|
+
GovernanceBlockedError,
|
|
374
|
+
GovernanceHaltError,
|
|
375
|
+
GuardrailsValidationError,
|
|
376
|
+
ApprovalRejectedError,
|
|
377
|
+
ApprovalTimeoutError,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
result = await governed.ainvoke({"messages": [...]}, config=...)
|
|
382
|
+
except GovernanceBlockedError as e:
|
|
383
|
+
print(f"Action blocked by policy: {e}")
|
|
384
|
+
except GovernanceHaltError as e:
|
|
385
|
+
print(f"Session halted: {e}")
|
|
386
|
+
except GuardrailsValidationError as e:
|
|
387
|
+
print(f"Guardrail triggered: {e}")
|
|
388
|
+
except ApprovalRejectedError as e:
|
|
389
|
+
print(f"Human rejected the action: {e}")
|
|
390
|
+
except ApprovalTimeoutError as e:
|
|
391
|
+
print(f"HITL approval timed out: {e}")
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
| Exception | When raised |
|
|
395
|
+
|---|---|
|
|
396
|
+
| `GovernanceBlockedError` | Policy returned `BLOCK` |
|
|
397
|
+
| `GovernanceHaltError` | Policy returned `HALT`, or HITL was rejected/expired |
|
|
398
|
+
| `GuardrailsValidationError` | Guardrail fired on an LLM prompt or tool output |
|
|
399
|
+
| `ApprovalRejectedError` | Human rejected a `REQUIRE_APPROVAL` decision |
|
|
400
|
+
| `ApprovalTimeoutError` | HITL polling exceeded `max_wait_ms` |
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Advanced usage
|
|
405
|
+
|
|
406
|
+
### Streaming
|
|
407
|
+
|
|
408
|
+
`astream_governed` mirrors LangGraph's `astream` and yields the original event stream while governance runs in the background:
|
|
409
|
+
|
|
410
|
+
```python
|
|
411
|
+
async for event in governed.astream_governed(
|
|
412
|
+
{"messages": [{"role": "user", "content": "..."}]},
|
|
413
|
+
config={"configurable": {"thread_id": "session-001"}},
|
|
414
|
+
stream_mode="values",
|
|
415
|
+
):
|
|
416
|
+
# process streamed events
|
|
417
|
+
pass
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Multi-turn sessions
|
|
421
|
+
|
|
422
|
+
Pass a consistent `thread_id` in `configurable` across turns to maintain conversation state:
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
config = {"configurable": {"thread_id": "user-42-session-7"}}
|
|
426
|
+
|
|
427
|
+
# Turn 1
|
|
428
|
+
await governed.ainvoke({"messages": [{"role": "user", "content": "Hello"}]}, config=config)
|
|
429
|
+
|
|
430
|
+
# Turn 2 — same session, governance sees the full history
|
|
431
|
+
await governed.ainvoke({"messages": [{"role": "user", "content": "Now export the data"}]}, config=config)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Skipping internal chains
|
|
435
|
+
|
|
436
|
+
LangGraph emits `on_chain_start` for many internal nodes (prompt templates, runnables, etc.). Skip nodes that don't need governance to reduce noise:
|
|
437
|
+
|
|
438
|
+
```python
|
|
439
|
+
governed = await create_openbox_graph_handler(
|
|
440
|
+
graph=agent,
|
|
441
|
+
...
|
|
442
|
+
skip_chain_types={
|
|
443
|
+
"agent",
|
|
444
|
+
"call_model",
|
|
445
|
+
"RunnableSequence",
|
|
446
|
+
"Prompt",
|
|
447
|
+
"ChatPromptTemplate",
|
|
448
|
+
},
|
|
449
|
+
)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### `fail_closed` mode
|
|
453
|
+
|
|
454
|
+
For high-sensitivity production agents, use `on_api_error="fail_closed"` to block all tool calls if OpenBox Core is unreachable:
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
governed = await create_openbox_graph_handler(
|
|
458
|
+
graph=agent,
|
|
459
|
+
on_api_error="fail_closed",
|
|
460
|
+
...
|
|
461
|
+
)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Debugging
|
|
467
|
+
|
|
468
|
+
Set `OPENBOX_DEBUG=1` to log all governance requests and responses:
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
OPENBOX_DEBUG=1 python agent.py
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Each governance exchange is printed to stdout:
|
|
475
|
+
|
|
476
|
+
```
|
|
477
|
+
[OpenBox Debug] governance request: { "event_type": "ActivityStarted", "activity_type": "search_web", ... }
|
|
478
|
+
[OpenBox Debug] governance response: { "verdict": "allow", ... }
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Contributing
|
|
484
|
+
|
|
485
|
+
Contributions are welcome! Please open an issue before submitting a large pull request.
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
git clone https://github.com/openbox-ai/openbox-langchain-sdk
|
|
489
|
+
cd sdk-langgraph-python
|
|
490
|
+
pip install -e ".[dev]"
|
|
491
|
+
pytest
|
|
492
|
+
```
|