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
|
File without changes
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Shared DeepAgents utilities for subagent detection and conflict guards.
|
|
2
|
+
|
|
3
|
+
Used by both the legacy OpenBoxDeepAgentHandler and the new OpenBoxMiddleware.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from openbox_langgraph.types import LangGraphStreamEvent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
17
|
+
# DeepAgents built-in tool names
|
|
18
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
DEEPAGENT_BUILTIN_TOOLS: frozenset[str] = frozenset({
|
|
21
|
+
"write_todos",
|
|
22
|
+
"ls",
|
|
23
|
+
"read_file",
|
|
24
|
+
"write_file",
|
|
25
|
+
"edit_file",
|
|
26
|
+
"glob",
|
|
27
|
+
"grep",
|
|
28
|
+
"execute",
|
|
29
|
+
"task",
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
DEEPAGENT_SUBAGENT_TOOL = "task"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
36
|
+
# Subagent name resolver (from LangGraph stream events)
|
|
37
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
38
|
+
|
|
39
|
+
def resolve_deepagent_subagent_name(event: LangGraphStreamEvent) -> str | None:
|
|
40
|
+
"""Detect DeepAgents subagent invocations from the `task` tool's `on_tool_start` event.
|
|
41
|
+
|
|
42
|
+
DeepAgents subagents run synchronously inside the `task` tool body — their
|
|
43
|
+
events are NOT visible in the outer stream. The only observable signal is
|
|
44
|
+
the `task` tool's `on_tool_start` event, which carries `subagent_type` in
|
|
45
|
+
its input dict.
|
|
46
|
+
|
|
47
|
+
Returns the `subagent_type` string (e.g. `"weather"`, `"general-purpose"`),
|
|
48
|
+
or None for all other events.
|
|
49
|
+
"""
|
|
50
|
+
if event.event != "on_tool_start":
|
|
51
|
+
return None
|
|
52
|
+
if event.name != DEEPAGENT_SUBAGENT_TOOL:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
raw_input = event.data.get("input")
|
|
56
|
+
if isinstance(raw_input, dict):
|
|
57
|
+
subagent_type = raw_input.get("subagent_type")
|
|
58
|
+
if isinstance(subagent_type, str):
|
|
59
|
+
return subagent_type
|
|
60
|
+
|
|
61
|
+
# Fallback: DeepAgents default is general-purpose
|
|
62
|
+
if sys.stderr and os.environ.get("OPENBOX_DEBUG"):
|
|
63
|
+
sys.stderr.write(
|
|
64
|
+
f"[OpenBox Debug] task tool input missing subagent_type, "
|
|
65
|
+
f"defaulting to general-purpose. raw_input={raw_input!r}\n"
|
|
66
|
+
)
|
|
67
|
+
return "general-purpose"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
71
|
+
# Subagent name resolver (from tool_call dict — used by middleware)
|
|
72
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
def resolve_subagent_from_tool_call(tool_name: str, tool_args: Any) -> str | None:
|
|
75
|
+
"""Extract subagent_type from a tool_call dict (middleware hook context).
|
|
76
|
+
|
|
77
|
+
Unlike resolve_deepagent_subagent_name which works on LangGraph stream events,
|
|
78
|
+
this works on the raw tool_call data available in wrap_tool_call hooks.
|
|
79
|
+
|
|
80
|
+
Returns the subagent_type string, or None if not a subagent tool.
|
|
81
|
+
"""
|
|
82
|
+
if tool_name != DEEPAGENT_SUBAGENT_TOOL:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
if isinstance(tool_args, dict):
|
|
86
|
+
subagent_type = tool_args.get("subagent_type")
|
|
87
|
+
if isinstance(subagent_type, str):
|
|
88
|
+
return subagent_type
|
|
89
|
+
|
|
90
|
+
# Fallback: DeepAgents default is general-purpose
|
|
91
|
+
if sys.stderr and os.environ.get("OPENBOX_DEBUG"):
|
|
92
|
+
sys.stderr.write(
|
|
93
|
+
f"[OpenBox Debug] task tool_call missing subagent_type, "
|
|
94
|
+
f"defaulting to general-purpose. tool_args={tool_args!r}\n"
|
|
95
|
+
)
|
|
96
|
+
return "general-purpose"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
100
|
+
# HITL / interrupt_on conflict detection
|
|
101
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
102
|
+
|
|
103
|
+
def hitl_enabled(hitl: Any) -> bool:
|
|
104
|
+
"""Return True if the HITL config indicates polling is enabled."""
|
|
105
|
+
if hitl is None:
|
|
106
|
+
return False
|
|
107
|
+
if isinstance(hitl, dict):
|
|
108
|
+
return bool(hitl.get("enabled", False))
|
|
109
|
+
return bool(getattr(hitl, "enabled", False))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def graph_has_interrupt_on(graph: Any) -> bool:
|
|
113
|
+
"""Check whether the compiled graph has interrupt_before or interrupt_after configured.
|
|
114
|
+
|
|
115
|
+
DeepAgents HumanInTheLoopMiddleware sets these on the compiled graph.
|
|
116
|
+
This is a best-effort check — false negatives are possible for custom setups.
|
|
117
|
+
"""
|
|
118
|
+
interrupt_before = (
|
|
119
|
+
getattr(graph, "interrupt_before", None)
|
|
120
|
+
or getattr(graph, "interruptBefore", None)
|
|
121
|
+
)
|
|
122
|
+
interrupt_after = (
|
|
123
|
+
getattr(graph, "interrupt_after", None)
|
|
124
|
+
or getattr(graph, "interruptAfter", None)
|
|
125
|
+
)
|
|
126
|
+
if isinstance(interrupt_before, (list, tuple, set)) and len(interrupt_before) > 0:
|
|
127
|
+
return True
|
|
128
|
+
if isinstance(interrupt_after, (list, tuple, set)) and len(interrupt_after) > 0:
|
|
129
|
+
return True
|
|
130
|
+
return False
|