openbox-deepagent-sdk-python 0.1.0__py3-none-any.whl
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_deepagent/__init__.py +91 -0
- openbox_deepagent/middleware.py +354 -0
- openbox_deepagent/middleware_factory.py +74 -0
- openbox_deepagent/middleware_hooks.py +783 -0
- openbox_deepagent/py.typed +0 -0
- openbox_deepagent/subagent_resolver.py +130 -0
- openbox_deepagent_sdk_python-0.1.0.dist-info/METADATA +739 -0
- openbox_deepagent_sdk_python-0.1.0.dist-info/RECORD +9 -0
- openbox_deepagent_sdk_python-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenBox DeepAgents SDK — governance middleware for DeepAgents graphs.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
>>> from openbox_deepagent import create_openbox_middleware
|
|
6
|
+
>>> middleware = create_openbox_middleware(
|
|
7
|
+
... api_url=os.environ["OPENBOX_URL"],
|
|
8
|
+
... api_key=os.environ["OPENBOX_API_KEY"],
|
|
9
|
+
... agent_name="MyBot",
|
|
10
|
+
... known_subagents=["researcher"],
|
|
11
|
+
... )
|
|
12
|
+
>>> agent = create_deep_agent(model="gpt-4o-mini", middleware=[middleware])
|
|
13
|
+
>>> result = await agent.ainvoke({"messages": [...]})
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
17
|
+
|
|
18
|
+
# Re-export the openbox-langgraph-sdk public surface
|
|
19
|
+
from openbox_langgraph import (
|
|
20
|
+
ApprovalExpiredError,
|
|
21
|
+
ApprovalRejectedError,
|
|
22
|
+
ApprovalTimeoutError,
|
|
23
|
+
GovernanceBlockedError,
|
|
24
|
+
GovernanceConfig,
|
|
25
|
+
GovernanceHaltError,
|
|
26
|
+
GovernanceVerdictResponse,
|
|
27
|
+
GuardrailsValidationError,
|
|
28
|
+
LangChainGovernanceEvent,
|
|
29
|
+
LangGraphStreamEvent,
|
|
30
|
+
OpenBoxAuthError,
|
|
31
|
+
OpenBoxError,
|
|
32
|
+
OpenBoxInsecureURLError,
|
|
33
|
+
OpenBoxLangGraphHandler,
|
|
34
|
+
OpenBoxLangGraphHandlerOptions,
|
|
35
|
+
OpenBoxNetworkError,
|
|
36
|
+
Verdict,
|
|
37
|
+
create_openbox_graph_handler,
|
|
38
|
+
get_global_config,
|
|
39
|
+
initialize,
|
|
40
|
+
rfc3339_now,
|
|
41
|
+
safe_serialize,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from openbox_deepagent.middleware import OpenBoxMiddleware, OpenBoxMiddlewareOptions
|
|
45
|
+
from openbox_deepagent.middleware_factory import create_openbox_middleware
|
|
46
|
+
from openbox_deepagent.subagent_resolver import (
|
|
47
|
+
DEEPAGENT_BUILTIN_TOOLS,
|
|
48
|
+
DEEPAGENT_SUBAGENT_TOOL,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
__version__ = version("openbox-deepagent-sdk-python")
|
|
53
|
+
except PackageNotFoundError:
|
|
54
|
+
__version__ = "unknown"
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
# Shared
|
|
58
|
+
"DEEPAGENT_BUILTIN_TOOLS",
|
|
59
|
+
"DEEPAGENT_SUBAGENT_TOOL",
|
|
60
|
+
"ApprovalExpiredError",
|
|
61
|
+
"ApprovalRejectedError",
|
|
62
|
+
"ApprovalTimeoutError",
|
|
63
|
+
"GovernanceBlockedError",
|
|
64
|
+
"GovernanceConfig",
|
|
65
|
+
"GovernanceHaltError",
|
|
66
|
+
"GovernanceVerdictResponse",
|
|
67
|
+
"GuardrailsValidationError",
|
|
68
|
+
"LangChainGovernanceEvent",
|
|
69
|
+
"LangGraphStreamEvent",
|
|
70
|
+
"OpenBoxAuthError",
|
|
71
|
+
# Errors
|
|
72
|
+
"OpenBoxError",
|
|
73
|
+
"OpenBoxInsecureURLError",
|
|
74
|
+
# Base handler
|
|
75
|
+
"OpenBoxLangGraphHandler",
|
|
76
|
+
"OpenBoxLangGraphHandlerOptions",
|
|
77
|
+
# Middleware API
|
|
78
|
+
"OpenBoxMiddleware",
|
|
79
|
+
"OpenBoxMiddlewareOptions",
|
|
80
|
+
"OpenBoxNetworkError",
|
|
81
|
+
# Types
|
|
82
|
+
"Verdict",
|
|
83
|
+
# Version
|
|
84
|
+
"__version__",
|
|
85
|
+
"create_openbox_graph_handler",
|
|
86
|
+
"create_openbox_middleware",
|
|
87
|
+
"get_global_config",
|
|
88
|
+
"initialize",
|
|
89
|
+
"rfc3339_now",
|
|
90
|
+
"safe_serialize",
|
|
91
|
+
]
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""OpenBox DeepAgents Middleware — LangChain AgentMiddleware for governance.
|
|
2
|
+
|
|
3
|
+
Replaces the astream_events-based handler with clean middleware hooks that fire
|
|
4
|
+
at exact execution points in the agent lifecycle:
|
|
5
|
+
|
|
6
|
+
abefore_agent → WorkflowStarted + SignalReceived + pre-screen guardrails
|
|
7
|
+
awrap_model_call → LLMStarted (PII redaction) → Model → LLMCompleted
|
|
8
|
+
awrap_tool_call → ToolStarted → Tool (OTel spans) → ToolCompleted
|
|
9
|
+
aafter_agent → WorkflowCompleted + cleanup
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from openbox_deepagent import create_openbox_middleware
|
|
13
|
+
middleware = create_openbox_middleware(api_url=..., api_key=..., agent_name="Bot")
|
|
14
|
+
agent = create_deep_agent(model="gpt-4o-mini", middleware=[middleware])
|
|
15
|
+
result = await agent.ainvoke({"messages": [...]})
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import concurrent.futures
|
|
22
|
+
import logging
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import TYPE_CHECKING, Any
|
|
25
|
+
|
|
26
|
+
from langchain.agents.middleware.types import AgentMiddleware, ModelRequest
|
|
27
|
+
from langgraph.prebuilt.tool_node import ToolCallRequest
|
|
28
|
+
from openbox_langgraph.client import GovernanceClient
|
|
29
|
+
from openbox_langgraph.config import GovernanceConfig, get_global_config, merge_config
|
|
30
|
+
from openbox_langgraph.types import GovernanceVerdictResponse
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from openbox_langgraph.span_processor import WorkflowSpanProcessor
|
|
34
|
+
|
|
35
|
+
_logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
39
|
+
# Options
|
|
40
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class OpenBoxMiddlewareOptions:
|
|
44
|
+
"""Configuration for OpenBoxMiddleware."""
|
|
45
|
+
|
|
46
|
+
agent_name: str | None = None
|
|
47
|
+
session_id: str | None = None
|
|
48
|
+
task_queue: str = "langgraph"
|
|
49
|
+
on_api_error: str = "fail_open"
|
|
50
|
+
governance_timeout: float = 30.0
|
|
51
|
+
known_subagents: list[str] = field(default_factory=lambda: ["general-purpose"])
|
|
52
|
+
tool_type_map: dict[str, str] = field(default_factory=dict)
|
|
53
|
+
skip_tool_types: set[str] = field(default_factory=set)
|
|
54
|
+
sqlalchemy_engine: Any = None
|
|
55
|
+
send_chain_start_event: bool = True
|
|
56
|
+
send_chain_end_event: bool = True
|
|
57
|
+
send_llm_start_event: bool = True
|
|
58
|
+
send_llm_end_event: bool = True
|
|
59
|
+
send_tool_start_event: bool = True
|
|
60
|
+
send_tool_end_event: bool = True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
64
|
+
# OpenBoxMiddleware
|
|
65
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
66
|
+
|
|
67
|
+
class OpenBoxMiddleware(AgentMiddleware):
|
|
68
|
+
"""LangChain AgentMiddleware implementing OpenBox governance for DeepAgents.
|
|
69
|
+
|
|
70
|
+
Hooks map directly to the governance event lifecycle:
|
|
71
|
+
- abefore_agent: session setup (WorkflowStarted, SignalReceived, pre-screen)
|
|
72
|
+
- awrap_model_call: LLM governance (LLMStarted/Completed, PII redaction)
|
|
73
|
+
- awrap_tool_call: tool governance (ToolStarted/Completed, SpanProcessor ctx)
|
|
74
|
+
- aafter_agent: session close (WorkflowCompleted, cleanup)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, options: OpenBoxMiddlewareOptions | None = None) -> None:
|
|
78
|
+
opts = options or OpenBoxMiddlewareOptions()
|
|
79
|
+
self._options = opts
|
|
80
|
+
|
|
81
|
+
# Build GovernanceConfig from options
|
|
82
|
+
self._config: GovernanceConfig = merge_config({
|
|
83
|
+
"on_api_error": opts.on_api_error,
|
|
84
|
+
"api_timeout": opts.governance_timeout,
|
|
85
|
+
"send_chain_start_event": opts.send_chain_start_event,
|
|
86
|
+
"send_chain_end_event": opts.send_chain_end_event,
|
|
87
|
+
"send_tool_start_event": opts.send_tool_start_event,
|
|
88
|
+
"send_tool_end_event": opts.send_tool_end_event,
|
|
89
|
+
"send_llm_start_event": opts.send_llm_start_event,
|
|
90
|
+
"send_llm_end_event": opts.send_llm_end_event,
|
|
91
|
+
"skip_tool_types": opts.skip_tool_types,
|
|
92
|
+
"session_id": opts.session_id,
|
|
93
|
+
"agent_name": opts.agent_name,
|
|
94
|
+
"task_queue": opts.task_queue,
|
|
95
|
+
"tool_type_map": opts.tool_type_map or {},
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
# Governance client
|
|
99
|
+
gc = get_global_config()
|
|
100
|
+
self._client = GovernanceClient(
|
|
101
|
+
api_url=gc.api_url,
|
|
102
|
+
api_key=gc.api_key,
|
|
103
|
+
timeout=gc.governance_timeout,
|
|
104
|
+
on_api_error=self._config.on_api_error,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# OTel span processor for hook-level governance
|
|
108
|
+
self._span_processor: WorkflowSpanProcessor | None = None
|
|
109
|
+
if gc.api_url and gc.api_key:
|
|
110
|
+
from openbox_langgraph.otel_setup import setup_opentelemetry_for_governance
|
|
111
|
+
from openbox_langgraph.span_processor import WorkflowSpanProcessor as WSP
|
|
112
|
+
self._span_processor = WSP()
|
|
113
|
+
setup_opentelemetry_for_governance(
|
|
114
|
+
span_processor=self._span_processor,
|
|
115
|
+
api_url=gc.api_url,
|
|
116
|
+
api_key=gc.api_key,
|
|
117
|
+
ignored_urls=[gc.api_url],
|
|
118
|
+
api_timeout=gc.governance_timeout,
|
|
119
|
+
on_api_error=self._config.on_api_error,
|
|
120
|
+
instrument_file_io=True,
|
|
121
|
+
sqlalchemy_engine=opts.sqlalchemy_engine,
|
|
122
|
+
)
|
|
123
|
+
# Suppress harmless OTel context detach errors from asyncio.Task
|
|
124
|
+
# boundaries in LangGraph — the token was attached in one task
|
|
125
|
+
# but detached in another, which ContextVar rejects.
|
|
126
|
+
logging.getLogger("opentelemetry.context").setLevel(logging.CRITICAL)
|
|
127
|
+
_logger.debug("[OpenBox] OTel HTTP governance hooks enabled (middleware)")
|
|
128
|
+
|
|
129
|
+
self._known_subagents: frozenset[str] = frozenset(opts.known_subagents)
|
|
130
|
+
|
|
131
|
+
# Reusable thread pool for sync-to-async bridge (avoids per-call overhead)
|
|
132
|
+
self._sync_executor: concurrent.futures.ThreadPoolExecutor | None = None
|
|
133
|
+
|
|
134
|
+
# Per-invocation state (reset in before_agent/abefore_agent)
|
|
135
|
+
self._sync_mode: bool = False
|
|
136
|
+
self._workflow_id: str = ""
|
|
137
|
+
self._run_id: str = ""
|
|
138
|
+
self._thread_id: str = ""
|
|
139
|
+
self._pre_screen_response: GovernanceVerdictResponse | None = None
|
|
140
|
+
self._first_llm_call: bool = True
|
|
141
|
+
|
|
142
|
+
# ─────────────────────────────────────────────────────────────
|
|
143
|
+
# Tool classification (ported from langgraph_handler.py)
|
|
144
|
+
# ─────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
def _resolve_tool_type(self, tool_name: str, subagent_name: str | None) -> str | None:
|
|
147
|
+
"""Resolve semantic tool_type for a given tool.
|
|
148
|
+
|
|
149
|
+
Priority: 1) explicit tool_type_map, 2) "a2a" if subagent, 3) None
|
|
150
|
+
"""
|
|
151
|
+
if tool_name in self._config.tool_type_map:
|
|
152
|
+
return self._config.tool_type_map[tool_name]
|
|
153
|
+
if subagent_name:
|
|
154
|
+
return "a2a"
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def _enrich_activity_input(
|
|
158
|
+
self,
|
|
159
|
+
base_input: list[Any] | None,
|
|
160
|
+
tool_type: str | None,
|
|
161
|
+
subagent_name: str | None,
|
|
162
|
+
) -> list[Any] | None:
|
|
163
|
+
"""Append __openbox metadata to activity_input for Rego policy use."""
|
|
164
|
+
if tool_type is None and subagent_name is None:
|
|
165
|
+
return base_input
|
|
166
|
+
meta: dict[str, Any] = {}
|
|
167
|
+
if tool_type is not None:
|
|
168
|
+
meta["tool_type"] = tool_type
|
|
169
|
+
if subagent_name is not None:
|
|
170
|
+
meta["subagent_name"] = subagent_name
|
|
171
|
+
result = list(base_input) if base_input else []
|
|
172
|
+
result.append({"__openbox": meta})
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
# ─────────────────────────────────────────────────────────────
|
|
176
|
+
# Subagent introspection
|
|
177
|
+
# ─────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
def get_known_subagents(self) -> list[str]:
|
|
180
|
+
"""Return the known subagent names registered with this middleware."""
|
|
181
|
+
return sorted(self._known_subagents)
|
|
182
|
+
|
|
183
|
+
# ─────────────────────────────────────────────────────────────
|
|
184
|
+
# Async-to-sync bridge
|
|
185
|
+
# ─────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
def _run_async(self, coro):
|
|
188
|
+
"""Run an async coroutine from sync context.
|
|
189
|
+
|
|
190
|
+
When LangGraph calls sync hooks from inside its event loop,
|
|
191
|
+
we must run in a thread pool. We copy the OTel context into
|
|
192
|
+
the thread so span propagation works correctly.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
loop = asyncio.get_running_loop()
|
|
196
|
+
except RuntimeError:
|
|
197
|
+
loop = None
|
|
198
|
+
if loop and loop.is_running():
|
|
199
|
+
# Inside LangGraph's event loop — run in thread with OTel context
|
|
200
|
+
from opentelemetry import context as otel_context
|
|
201
|
+
ctx = otel_context.get_current()
|
|
202
|
+
|
|
203
|
+
def _run_with_ctx():
|
|
204
|
+
token = otel_context.attach(ctx)
|
|
205
|
+
try:
|
|
206
|
+
return asyncio.run(coro)
|
|
207
|
+
finally:
|
|
208
|
+
try:
|
|
209
|
+
otel_context.detach(token)
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
if self._sync_executor is None:
|
|
214
|
+
self._sync_executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
|
215
|
+
return self._sync_executor.submit(_run_with_ctx).result()
|
|
216
|
+
return asyncio.run(coro)
|
|
217
|
+
|
|
218
|
+
# ─────────────────────────────────────────────────────────────
|
|
219
|
+
# Sync middleware hooks — for invoke()/stream() callers
|
|
220
|
+
# ─────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
def before_agent(self, state, runtime) -> dict[str, Any] | None:
|
|
223
|
+
"""Sync session setup: delegates to async via _run_async."""
|
|
224
|
+
self._sync_mode = True
|
|
225
|
+
if self._span_processor:
|
|
226
|
+
self._span_processor.set_sync_mode(True)
|
|
227
|
+
from openbox_deepagent.middleware_hooks import handle_before_agent
|
|
228
|
+
return self._run_async(handle_before_agent(self, state, runtime))
|
|
229
|
+
|
|
230
|
+
def after_agent(self, state, runtime) -> dict[str, Any] | None:
|
|
231
|
+
"""Sync session close: send WorkflowCompleted via sync httpx."""
|
|
232
|
+
from openbox_deepagent.middleware_hooks import handle_after_agent
|
|
233
|
+
self._run_async(handle_after_agent(self, state, runtime))
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
def wrap_model_call(self, request: ModelRequest, handler) -> Any:
|
|
237
|
+
"""Sync LLM governance with direct OTel span in current thread.
|
|
238
|
+
|
|
239
|
+
Creates OTel span and registers trace_id in the sync thread so httpx
|
|
240
|
+
hooks can find the activity context (avoids asyncio.run ContextVar
|
|
241
|
+
fragmentation).
|
|
242
|
+
"""
|
|
243
|
+
from opentelemetry import context as otel_ctx
|
|
244
|
+
from opentelemetry import trace as otel_tr
|
|
245
|
+
|
|
246
|
+
from openbox_deepagent.middleware_hooks import handle_wrap_model_call
|
|
247
|
+
|
|
248
|
+
# Create OTel span in sync thread for httpx hook visibility
|
|
249
|
+
tracer = otel_tr.get_tracer("openbox-deepagent")
|
|
250
|
+
span = tracer.start_span("llm.call.sync", kind=otel_tr.SpanKind.INTERNAL)
|
|
251
|
+
token = otel_ctx.attach(otel_tr.set_span_in_context(span))
|
|
252
|
+
trace_id = span.get_span_context().trace_id
|
|
253
|
+
activity_id = None
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
# Run the async governance handler — it will register its own
|
|
257
|
+
# trace_id via _run_with_otel_context, but we also register the
|
|
258
|
+
# sync thread's trace_id so httpx sync hooks can find it
|
|
259
|
+
async def async_handler(req):
|
|
260
|
+
return handler(req)
|
|
261
|
+
|
|
262
|
+
async def _wrapped():
|
|
263
|
+
nonlocal activity_id
|
|
264
|
+
# Import here to get the activity_id from the handler
|
|
265
|
+
import uuid
|
|
266
|
+
activity_id = str(uuid.uuid4())
|
|
267
|
+
if self._span_processor and trace_id:
|
|
268
|
+
self._span_processor.register_trace(trace_id, self._workflow_id, activity_id)
|
|
269
|
+
self._span_processor.set_activity_context(self._workflow_id, activity_id, {
|
|
270
|
+
"source": "workflow-telemetry",
|
|
271
|
+
"workflow_id": self._workflow_id,
|
|
272
|
+
"run_id": self._run_id,
|
|
273
|
+
"event_type": "ActivityStarted",
|
|
274
|
+
"activity_id": activity_id,
|
|
275
|
+
"activity_type": "llm_call",
|
|
276
|
+
})
|
|
277
|
+
return await handle_wrap_model_call(self, request, async_handler)
|
|
278
|
+
|
|
279
|
+
return self._run_async(_wrapped())
|
|
280
|
+
finally:
|
|
281
|
+
span.end()
|
|
282
|
+
try:
|
|
283
|
+
otel_ctx.detach(token)
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
def wrap_tool_call(self, request: ToolCallRequest, handler) -> Any:
|
|
288
|
+
"""Sync tool governance with direct OTel span in current thread."""
|
|
289
|
+
from opentelemetry import context as otel_ctx
|
|
290
|
+
from opentelemetry import trace as otel_tr
|
|
291
|
+
|
|
292
|
+
from openbox_deepagent.middleware_hooks import handle_wrap_tool_call
|
|
293
|
+
|
|
294
|
+
tracer = otel_tr.get_tracer("openbox-deepagent")
|
|
295
|
+
tool_name = (
|
|
296
|
+
request.tool_call.get("name", "tool")
|
|
297
|
+
if hasattr(request, "tool_call") else "tool"
|
|
298
|
+
)
|
|
299
|
+
span = tracer.start_span(f"tool.{tool_name}.sync", kind=otel_tr.SpanKind.INTERNAL)
|
|
300
|
+
token = otel_ctx.attach(otel_tr.set_span_in_context(span))
|
|
301
|
+
trace_id = span.get_span_context().trace_id
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
async def async_handler(req):
|
|
305
|
+
return handler(req)
|
|
306
|
+
|
|
307
|
+
# Register sync thread trace_id before running handler
|
|
308
|
+
if self._span_processor and trace_id:
|
|
309
|
+
import uuid
|
|
310
|
+
sync_activity_id = str(uuid.uuid4())
|
|
311
|
+
self._span_processor.register_trace(trace_id, self._workflow_id, sync_activity_id)
|
|
312
|
+
self._span_processor.set_activity_context(self._workflow_id, sync_activity_id, {
|
|
313
|
+
"source": "workflow-telemetry",
|
|
314
|
+
"workflow_id": self._workflow_id,
|
|
315
|
+
"run_id": self._run_id,
|
|
316
|
+
"event_type": "ActivityStarted",
|
|
317
|
+
"activity_id": sync_activity_id,
|
|
318
|
+
"activity_type": tool_name,
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
return self._run_async(handle_wrap_tool_call(self, request, async_handler))
|
|
322
|
+
finally:
|
|
323
|
+
span.end()
|
|
324
|
+
try:
|
|
325
|
+
otel_ctx.detach(token)
|
|
326
|
+
except Exception:
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
# ─────────────────────────────────────────────────────────────
|
|
330
|
+
# Async middleware hooks — for ainvoke()/astream() callers
|
|
331
|
+
# ─────────────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
async def abefore_agent(self, state, runtime) -> dict[str, Any] | None:
|
|
334
|
+
"""Session setup: WorkflowStarted + SignalReceived + pre-screen guardrails."""
|
|
335
|
+
self._sync_mode = False
|
|
336
|
+
if self._span_processor:
|
|
337
|
+
self._span_processor.set_sync_mode(False)
|
|
338
|
+
from openbox_deepagent.middleware_hooks import handle_before_agent
|
|
339
|
+
return await handle_before_agent(self, state, runtime)
|
|
340
|
+
|
|
341
|
+
async def aafter_agent(self, state, runtime) -> dict[str, Any] | None:
|
|
342
|
+
"""Session close: WorkflowCompleted + cleanup."""
|
|
343
|
+
from openbox_deepagent.middleware_hooks import handle_after_agent
|
|
344
|
+
return await handle_after_agent(self, state, runtime)
|
|
345
|
+
|
|
346
|
+
async def awrap_model_call(self, request: ModelRequest, handler) -> Any:
|
|
347
|
+
"""LLM governance: LLMStarted → PII redaction → Model → LLMCompleted."""
|
|
348
|
+
from openbox_deepagent.middleware_hooks import handle_wrap_model_call
|
|
349
|
+
return await handle_wrap_model_call(self, request, handler)
|
|
350
|
+
|
|
351
|
+
async def awrap_tool_call(self, request: ToolCallRequest, handler) -> Any:
|
|
352
|
+
"""Tool governance: ToolStarted → Tool (OTel spans) → ToolCompleted."""
|
|
353
|
+
from openbox_deepagent.middleware_hooks import handle_wrap_tool_call
|
|
354
|
+
return await handle_wrap_tool_call(self, request, handler)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Factory for creating configured OpenBoxMiddleware instances.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
middleware = create_openbox_middleware(
|
|
5
|
+
api_url=os.environ["OPENBOX_URL"],
|
|
6
|
+
api_key=os.environ["OPENBOX_API_KEY"],
|
|
7
|
+
agent_name="ResearchBot",
|
|
8
|
+
known_subagents=["researcher", "writer"],
|
|
9
|
+
)
|
|
10
|
+
agent = create_deep_agent(
|
|
11
|
+
model=init_chat_model("openai:gpt-4o-mini"),
|
|
12
|
+
middleware=[middleware],
|
|
13
|
+
tools=[...],
|
|
14
|
+
)
|
|
15
|
+
result = await agent.ainvoke({"messages": [...]})
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import dataclasses
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from openbox_deepagent.middleware import OpenBoxMiddleware, OpenBoxMiddlewareOptions
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_openbox_middleware(
|
|
27
|
+
*,
|
|
28
|
+
api_url: str,
|
|
29
|
+
api_key: str,
|
|
30
|
+
agent_name: str | None = None,
|
|
31
|
+
governance_timeout: float = 30.0,
|
|
32
|
+
validate: bool = True,
|
|
33
|
+
known_subagents: list[str] | None = None,
|
|
34
|
+
sqlalchemy_engine: Any = None,
|
|
35
|
+
**kwargs: Any,
|
|
36
|
+
) -> OpenBoxMiddleware:
|
|
37
|
+
"""Create a configured OpenBoxMiddleware for create_deep_agent(middleware=[...]).
|
|
38
|
+
|
|
39
|
+
Validates the API key and sets up global config before returning the middleware.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
api_url: Base URL of your OpenBox Core instance.
|
|
43
|
+
api_key: API key in ``obx_live_*`` or ``obx_test_*`` format.
|
|
44
|
+
agent_name: Agent name as configured in the dashboard.
|
|
45
|
+
governance_timeout: HTTP timeout in seconds for governance calls (default 30.0).
|
|
46
|
+
validate: If True, validates the API key against the server on startup.
|
|
47
|
+
known_subagents: Subagent names from ``create_deep_agent(subagents=[...])``.
|
|
48
|
+
Defaults to ``["general-purpose"]``.
|
|
49
|
+
sqlalchemy_engine: Optional SQLAlchemy Engine instance to instrument for DB
|
|
50
|
+
governance. Required when the engine is created before the middleware
|
|
51
|
+
(e.g. ``SQLDatabase.from_uri()``). Without this, only engines created
|
|
52
|
+
after middleware initialization will be instrumented.
|
|
53
|
+
**kwargs: Additional keyword arguments forwarded to ``OpenBoxMiddlewareOptions``.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A configured ``OpenBoxMiddleware`` ready for injection into create_deep_agent.
|
|
57
|
+
"""
|
|
58
|
+
from openbox_langgraph.config import initialize
|
|
59
|
+
initialize(
|
|
60
|
+
api_url=api_url,
|
|
61
|
+
api_key=api_key,
|
|
62
|
+
governance_timeout=governance_timeout,
|
|
63
|
+
validate=validate,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
valid_fields = {f.name for f in dataclasses.fields(OpenBoxMiddlewareOptions)}
|
|
67
|
+
options = OpenBoxMiddlewareOptions(
|
|
68
|
+
agent_name=agent_name,
|
|
69
|
+
governance_timeout=governance_timeout,
|
|
70
|
+
known_subagents=known_subagents or ["general-purpose"],
|
|
71
|
+
sqlalchemy_engine=sqlalchemy_engine,
|
|
72
|
+
**{k: v for k, v in kwargs.items() if k in valid_fields},
|
|
73
|
+
)
|
|
74
|
+
return OpenBoxMiddleware(options)
|