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.
Files changed (36) hide show
  1. openbox_langgraph_sdk_python-0.1.0/.github/workflows/publish.yml +71 -0
  2. openbox_langgraph_sdk_python-0.1.0/.gitignore +2 -0
  3. openbox_langgraph_sdk_python-0.1.0/PKG-INFO +492 -0
  4. openbox_langgraph_sdk_python-0.1.0/README.md +455 -0
  5. openbox_langgraph_sdk_python-0.1.0/docs/code-standards.md +523 -0
  6. openbox_langgraph_sdk_python-0.1.0/docs/codebase-summary.md +308 -0
  7. openbox_langgraph_sdk_python-0.1.0/docs/project-overview-pdr.md +187 -0
  8. openbox_langgraph_sdk_python-0.1.0/docs/project-roadmap.md +327 -0
  9. openbox_langgraph_sdk_python-0.1.0/docs/system-architecture.md +603 -0
  10. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/__init__.py +130 -0
  11. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/client.py +358 -0
  12. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/config.py +264 -0
  13. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/db_governance_hooks.py +897 -0
  14. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/errors.py +114 -0
  15. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/file_governance_hooks.py +413 -0
  16. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/hitl.py +88 -0
  17. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/hook_governance.py +397 -0
  18. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/http_governance_hooks.py +695 -0
  19. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/langgraph_handler.py +1616 -0
  20. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/otel_setup.py +468 -0
  21. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/span_processor.py +253 -0
  22. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/tracing.py +352 -0
  23. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/types.py +485 -0
  24. openbox_langgraph_sdk_python-0.1.0/openbox_langgraph/verdict_handler.py +203 -0
  25. openbox_langgraph_sdk_python-0.1.0/pyproject.toml +63 -0
  26. openbox_langgraph_sdk_python-0.1.0/test-agent/.env +11 -0
  27. openbox_langgraph_sdk_python-0.1.0/test-agent/.env.example +9 -0
  28. openbox_langgraph_sdk_python-0.1.0/test-agent/README.md +27 -0
  29. openbox_langgraph_sdk_python-0.1.0/test-agent/SETUP.md +455 -0
  30. openbox_langgraph_sdk_python-0.1.0/test-agent/agent.py +172 -0
  31. openbox_langgraph_sdk_python-0.1.0/test-agent/pyproject.toml +16 -0
  32. openbox_langgraph_sdk_python-0.1.0/test-agent/uv.lock +1218 -0
  33. openbox_langgraph_sdk_python-0.1.0/tests/test_contextvars_propagation.py +59 -0
  34. openbox_langgraph_sdk_python-0.1.0/tests/test_governance_changes.py +279 -0
  35. openbox_langgraph_sdk_python-0.1.0/tests/test_telemetry_payload.py +333 -0
  36. 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,2 @@
1
+ /openbox_langgraph/__pycache__
2
+ /tests/__pycache__
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/openbox-langgraph-sdk)](https://pypi.org/project/openbox-langgraph-sdk/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/openbox-langgraph-sdk)](https://pypi.org/project/openbox-langgraph-sdk/)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ ```