hdsp-jupyter-extension 2.0.26__py3-none-any.whl → 2.0.28__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.
- agent_server/context_providers/__init__.py +4 -2
- agent_server/context_providers/actions.py +73 -7
- agent_server/context_providers/file.py +23 -23
- agent_server/langchain/__init__.py +2 -2
- agent_server/langchain/agent.py +18 -251
- agent_server/langchain/agent_factory.py +26 -4
- agent_server/langchain/agent_prompts/planner_prompt.py +22 -35
- agent_server/langchain/custom_middleware.py +278 -43
- agent_server/langchain/llm_factory.py +102 -54
- agent_server/langchain/logging_utils.py +1 -1
- agent_server/langchain/middleware/__init__.py +5 -0
- agent_server/langchain/middleware/code_history_middleware.py +126 -37
- agent_server/langchain/middleware/content_injection_middleware.py +110 -0
- agent_server/langchain/middleware/subagent_events.py +88 -9
- agent_server/langchain/middleware/subagent_middleware.py +518 -240
- agent_server/langchain/prompts.py +5 -22
- agent_server/langchain/state_schema.py +44 -0
- agent_server/langchain/tools/jupyter_tools.py +4 -5
- agent_server/langchain/tools/tool_registry.py +6 -0
- agent_server/routers/chat.py +305 -2
- agent_server/routers/config.py +193 -8
- agent_server/routers/config_schema.py +254 -0
- agent_server/routers/context.py +31 -8
- agent_server/routers/langchain_agent.py +310 -153
- hdsp_agent_core/managers/config_manager.py +100 -1
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
- hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js → hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.55727265b00191e68d9a.js +479 -15
- hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.55727265b00191e68d9a.js.map +1 -0
- jupyter_ext/labextension/static/lib_index_js.67505497667f9c0a763d.js → hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.df05d90f366bfd5fa023.js +1287 -190
- hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.df05d90f366bfd5fa023.js.map +1 -0
- hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.0fe2dcbbd176ee0efceb.js → hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.08fce819ee32e9d25175.js +3 -3
- jupyter_ext/labextension/static/remoteEntry.0fe2dcbbd176ee0efceb.js.map → hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.08fce819ee32e9d25175.js.map +1 -1
- {hdsp_jupyter_extension-2.0.26.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/METADATA +1 -1
- {hdsp_jupyter_extension-2.0.26.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/RECORD +66 -64
- jupyter_ext/_version.py +1 -1
- jupyter_ext/handlers.py +41 -0
- jupyter_ext/labextension/build_log.json +1 -1
- jupyter_ext/labextension/package.json +2 -2
- jupyter_ext/labextension/static/{frontend_styles_index_js.b5e4416b4e07ec087aad.js → frontend_styles_index_js.55727265b00191e68d9a.js} +479 -15
- jupyter_ext/labextension/static/frontend_styles_index_js.55727265b00191e68d9a.js.map +1 -0
- hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js → jupyter_ext/labextension/static/lib_index_js.df05d90f366bfd5fa023.js +1287 -190
- jupyter_ext/labextension/static/lib_index_js.df05d90f366bfd5fa023.js.map +1 -0
- jupyter_ext/labextension/static/{remoteEntry.0fe2dcbbd176ee0efceb.js → remoteEntry.08fce819ee32e9d25175.js} +3 -3
- hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.0fe2dcbbd176ee0efceb.js.map → jupyter_ext/labextension/static/remoteEntry.08fce819ee32e9d25175.js.map +1 -1
- agent_server/langchain/middleware/description_injector.py +0 -150
- hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js.map +0 -1
- hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js.map +0 -1
- jupyter_ext/labextension/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js.map +0 -1
- jupyter_ext/labextension/static/lib_index_js.67505497667f9c0a763d.js.map +0 -1
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js.map +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js.map +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js.map +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js.map +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +0 -0
- {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -0
- {hdsp_jupyter_extension-2.0.26.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.26.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,29 +2,43 @@
|
|
|
2
2
|
SubAgentMiddleware
|
|
3
3
|
|
|
4
4
|
Middleware that enables subagent delegation via the `task` tool.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
5
|
+
Benchmarked from Deep Agents library pattern.
|
|
6
|
+
|
|
7
|
+
Architecture:
|
|
8
|
+
- Uses ToolRuntime for state access (instead of global variables)
|
|
9
|
+
- Returns Command for state updates (instead of plain strings)
|
|
10
|
+
- Extracts generated code/SQL to state fields (avoids JSON escaping)
|
|
11
|
+
- ContentInjectionMiddleware handles injection into target tools
|
|
12
|
+
|
|
13
|
+
Key patterns from Deep Agents:
|
|
14
|
+
- _EXCLUDED_STATE_KEYS: isolates messages/todos between parent and subagent
|
|
15
|
+
- Command(update={...}): state update return from tools
|
|
16
|
+
- ToolRuntime.state: access graph state from within tools
|
|
13
17
|
"""
|
|
14
18
|
|
|
19
|
+
import contextvars
|
|
15
20
|
import hashlib
|
|
16
21
|
import json
|
|
17
22
|
import logging
|
|
18
|
-
|
|
23
|
+
import re
|
|
24
|
+
import time
|
|
25
|
+
import uuid
|
|
26
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
19
27
|
|
|
20
|
-
from langchain_core.
|
|
28
|
+
from langchain_core.messages import HumanMessage, ToolMessage
|
|
29
|
+
from langchain_core.tools import StructuredTool
|
|
21
30
|
from pydantic import BaseModel, Field
|
|
22
31
|
|
|
23
|
-
if TYPE_CHECKING:
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
32
|
logger = logging.getLogger(__name__)
|
|
27
33
|
|
|
34
|
+
# Deep Agents pattern: state keys excluded from bidirectional sharing
|
|
35
|
+
_EXCLUDED_STATE_KEYS = ("messages", "todos")
|
|
36
|
+
|
|
37
|
+
# Context variable to track the current main agent's thread_id
|
|
38
|
+
_current_thread_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
39
|
+
"current_thread_id", default=None
|
|
40
|
+
)
|
|
41
|
+
|
|
28
42
|
# Global registry for subagent factories (set by AgentFactory)
|
|
29
43
|
_subagent_factory = None
|
|
30
44
|
_current_llm_config = None
|
|
@@ -32,15 +46,15 @@ _current_llm_config = None
|
|
|
32
46
|
_subagent_cache: Dict[str, Any] = {}
|
|
33
47
|
|
|
34
48
|
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Global factory management (backward-compatible)
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
35
53
|
def set_subagent_factory(factory_func, llm_config: Dict[str, Any]):
|
|
36
|
-
"""
|
|
37
|
-
Set the subagent factory function.
|
|
38
|
-
Called by AgentFactory during initialization.
|
|
39
|
-
"""
|
|
54
|
+
"""Set the subagent factory function. Called by AgentFactory."""
|
|
40
55
|
global _subagent_factory, _current_llm_config, _subagent_cache
|
|
41
56
|
_subagent_factory = factory_func
|
|
42
57
|
_current_llm_config = llm_config
|
|
43
|
-
# Clear cache when factory changes (new LLM config)
|
|
44
58
|
_subagent_cache.clear()
|
|
45
59
|
logger.info("SubAgentMiddleware factory initialized (cache cleared)")
|
|
46
60
|
|
|
@@ -59,12 +73,7 @@ def _get_config_hash(llm_config: Dict[str, Any]) -> str:
|
|
|
59
73
|
def get_or_create_subagent(
|
|
60
74
|
agent_name: str, factory_func, llm_config: Dict[str, Any]
|
|
61
75
|
) -> Any:
|
|
62
|
-
"""
|
|
63
|
-
Get cached subagent or create new one.
|
|
64
|
-
|
|
65
|
-
Caching avoids expensive recompilation of LangGraph agents.
|
|
66
|
-
Cache key = "{agent_name}_{config_hash}" to handle different LLM configs.
|
|
67
|
-
"""
|
|
76
|
+
"""Get cached subagent or create new one."""
|
|
68
77
|
global _subagent_cache
|
|
69
78
|
|
|
70
79
|
config_hash = _get_config_hash(llm_config)
|
|
@@ -80,43 +89,330 @@ def get_or_create_subagent(
|
|
|
80
89
|
logger.info(
|
|
81
90
|
f"Cached subagent '{agent_name}' (total cached: {len(_subagent_cache)})"
|
|
82
91
|
)
|
|
83
|
-
|
|
84
92
|
return subagent
|
|
85
93
|
|
|
86
94
|
|
|
87
95
|
def clear_subagent_cache():
|
|
88
|
-
"""Clear the subagent cache.
|
|
96
|
+
"""Clear the subagent cache."""
|
|
89
97
|
global _subagent_cache
|
|
90
98
|
count = len(_subagent_cache)
|
|
91
99
|
_subagent_cache.clear()
|
|
92
100
|
logger.info(f"Subagent cache cleared ({count} entries removed)")
|
|
93
101
|
|
|
94
102
|
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Thread ID tracking (for code history middleware)
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def set_current_thread_id(thread_id: str) -> None:
|
|
108
|
+
"""Set the current main agent's thread_id for code history tracking."""
|
|
109
|
+
_current_thread_id.set(thread_id)
|
|
110
|
+
logger.debug(f"Set current thread_id: {thread_id}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_current_thread_id() -> Optional[str]:
|
|
114
|
+
"""Get the current main agent's thread_id."""
|
|
115
|
+
return _current_thread_id.get()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Content extraction utilities
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def extract_code_block(response: str, lang: str = "python") -> Optional[str]:
|
|
123
|
+
"""Extract code/SQL from response using [CODE]/[SQL] markers or fenced blocks.
|
|
124
|
+
|
|
125
|
+
Tries in order:
|
|
126
|
+
1. [CODE] or [SQL] section marker
|
|
127
|
+
2. ```python or ```sql fenced code block
|
|
128
|
+
3. ``` generic fenced code block
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
response: Full response text from subagent
|
|
132
|
+
lang: Language type ("python" or "sql")
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Extracted code string, or None if not found
|
|
136
|
+
"""
|
|
137
|
+
if not response:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
# Pattern 1: [CODE] or [SQL] section marker with fenced block
|
|
141
|
+
marker = "[CODE]" if lang == "python" else "[SQL]"
|
|
142
|
+
marker_pattern = re.escape(marker) + r'\s*\n\s*```(?:\w+)?\s*\n(.*?)```'
|
|
143
|
+
match = re.search(marker_pattern, response, re.DOTALL)
|
|
144
|
+
if match:
|
|
145
|
+
code = match.group(1).strip()
|
|
146
|
+
if code:
|
|
147
|
+
return code
|
|
148
|
+
|
|
149
|
+
# Pattern 2: [CODE] or [SQL] marker with content after it (no fenced block)
|
|
150
|
+
marker_pattern2 = re.escape(marker) + r'\s*\n(.*?)(?=\n\[|\Z)'
|
|
151
|
+
match = re.search(marker_pattern2, response, re.DOTALL)
|
|
152
|
+
if match:
|
|
153
|
+
code = match.group(1).strip()
|
|
154
|
+
# Remove fenced block markers if present
|
|
155
|
+
if code.startswith(f"```{lang}"):
|
|
156
|
+
code = code[len(f"```{lang}"):].strip()
|
|
157
|
+
if code.startswith("```"):
|
|
158
|
+
code = code[3:].strip()
|
|
159
|
+
if code.endswith("```"):
|
|
160
|
+
code = code[:-3].strip()
|
|
161
|
+
if code:
|
|
162
|
+
return code
|
|
163
|
+
|
|
164
|
+
# Pattern 3: Fenced code block with language
|
|
165
|
+
fenced_pattern = rf'```{lang}\s*\n(.*?)```'
|
|
166
|
+
match = re.search(fenced_pattern, response, re.DOTALL)
|
|
167
|
+
if match:
|
|
168
|
+
code = match.group(1).strip()
|
|
169
|
+
if code:
|
|
170
|
+
return code
|
|
171
|
+
|
|
172
|
+
# Pattern 4: Generic fenced code block (fallback)
|
|
173
|
+
generic_pattern = r'```\s*\n(.*?)```'
|
|
174
|
+
match = re.search(generic_pattern, response, re.DOTALL)
|
|
175
|
+
if match:
|
|
176
|
+
code = match.group(1).strip()
|
|
177
|
+
if code and len(code) > 10: # Minimum meaningful code
|
|
178
|
+
return code
|
|
179
|
+
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def extract_description(response: str) -> Optional[str]:
|
|
184
|
+
"""Extract [DESCRIPTION] section from subagent response.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
response: Full response text from subagent
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Description string, or None if not found
|
|
191
|
+
"""
|
|
192
|
+
if not response:
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
patterns = [
|
|
196
|
+
r'\[DESCRIPTION\]\s*\n(.*?)(?=\n\s*\[(?:CODE|SQL)\])',
|
|
197
|
+
r'\[DESCRIPTION\]\s*(.*?)(?=\s*\[(?:CODE|SQL)\])',
|
|
198
|
+
r'\[DESCRIPTION\]\s*\n(.*?)(?=\n\s*```)',
|
|
199
|
+
r'\[DESCRIPTION\]\s*\n(.+?)(?=\n\n|\Z)',
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
for pattern in patterns:
|
|
203
|
+
match = re.search(pattern, response, re.DOTALL | re.IGNORECASE)
|
|
204
|
+
if match:
|
|
205
|
+
description = match.group(1).strip()
|
|
206
|
+
if description and len(description) > 5:
|
|
207
|
+
return description
|
|
208
|
+
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def extract_generated_content(
|
|
213
|
+
response: str, agent_type: str
|
|
214
|
+
) -> tuple:
|
|
215
|
+
"""Extract code/SQL from subagent response based on agent type.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
response: Full response text from subagent
|
|
219
|
+
agent_type: "python_developer", "athena_query", etc.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Tuple of (content, content_type) or (None, None)
|
|
223
|
+
"""
|
|
224
|
+
if agent_type == "python_developer":
|
|
225
|
+
code = extract_code_block(response, lang="python")
|
|
226
|
+
return (code, "python") if code else (None, None)
|
|
227
|
+
elif agent_type == "athena_query":
|
|
228
|
+
sql = extract_code_block(response, lang="sql")
|
|
229
|
+
return (sql, "sql") if sql else (None, None)
|
|
230
|
+
return (None, None)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _build_summary(
|
|
234
|
+
response: str,
|
|
235
|
+
agent_type: str,
|
|
236
|
+
content_extracted: bool = False,
|
|
237
|
+
) -> str:
|
|
238
|
+
"""Build ToolMessage content for the caller.
|
|
239
|
+
|
|
240
|
+
When content is extracted to state, returns a short summary.
|
|
241
|
+
Otherwise returns the full response.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
response: Full response from subagent
|
|
245
|
+
agent_type: Type of the subagent
|
|
246
|
+
content_extracted: Whether content was extracted to state
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Summary or full response string
|
|
250
|
+
"""
|
|
251
|
+
if not content_extracted:
|
|
252
|
+
return response
|
|
253
|
+
|
|
254
|
+
# Content extracted - return summary only (actual content is in state)
|
|
255
|
+
desc = extract_description(response)
|
|
256
|
+
desc_part = f"\n설명: {desc}" if desc else ""
|
|
257
|
+
|
|
258
|
+
if agent_type == "python_developer":
|
|
259
|
+
return (
|
|
260
|
+
f"python_developer 코드 생성 완료.{desc_part}\n"
|
|
261
|
+
"jupyter_cell_tool() 또는 write_file_tool()로 실행/저장하세요. "
|
|
262
|
+
"(코드는 자동 주입됩니다)"
|
|
263
|
+
)
|
|
264
|
+
elif agent_type == "athena_query":
|
|
265
|
+
return (
|
|
266
|
+
f"athena_query SQL 생성 완료.{desc_part}\n"
|
|
267
|
+
"markdown_tool()로 표시하세요. "
|
|
268
|
+
"(SQL은 자동 주입됩니다)"
|
|
269
|
+
)
|
|
270
|
+
return response
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
# Task tool creation
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
def _invoke_subagent(
|
|
278
|
+
caller_name: str,
|
|
279
|
+
agent_name: str,
|
|
280
|
+
description: str,
|
|
281
|
+
context: Optional[str],
|
|
282
|
+
runtime_state: Optional[Dict[str, Any]] = None,
|
|
283
|
+
thread_id: Optional[str] = None,
|
|
284
|
+
) -> Dict[str, Any]:
|
|
285
|
+
"""Execute a subagent and return its result dict.
|
|
286
|
+
|
|
287
|
+
Shared logic between Main Agent task tool and nested task tool.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
caller_name: Name of the calling agent
|
|
291
|
+
agent_name: Name of the subagent to invoke
|
|
292
|
+
description: Task description
|
|
293
|
+
context: Optional additional context
|
|
294
|
+
runtime_state: Optional state from ToolRuntime (for state sharing)
|
|
295
|
+
thread_id: Optional main agent thread_id (for code history lookup)
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Result dict from subagent.invoke()
|
|
299
|
+
"""
|
|
300
|
+
from agent_server.langchain.middleware.subagent_events import (
|
|
301
|
+
clear_current_subagent,
|
|
302
|
+
emit_subagent_complete,
|
|
303
|
+
emit_subagent_start,
|
|
304
|
+
set_current_subagent,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
emit_subagent_start(agent_name, description)
|
|
308
|
+
|
|
309
|
+
factory_func, llm_config = get_subagent_factory()
|
|
310
|
+
if factory_func is None:
|
|
311
|
+
raise RuntimeError("SubAgentMiddleware not initialized. Call set_subagent_factory first.")
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
set_current_subagent(agent_name)
|
|
315
|
+
|
|
316
|
+
t0 = time.time()
|
|
317
|
+
subagent = get_or_create_subagent(agent_name, factory_func, llm_config)
|
|
318
|
+
logger.info(f"[TIMING] get_or_create_subagent took {time.time()-t0:.2f}s")
|
|
319
|
+
|
|
320
|
+
# Inject code history for python_developer
|
|
321
|
+
enhanced_context = context
|
|
322
|
+
if agent_name == "python_developer":
|
|
323
|
+
try:
|
|
324
|
+
from agent_server.langchain.middleware.code_history_middleware import (
|
|
325
|
+
get_code_history_tracker,
|
|
326
|
+
get_context_with_history,
|
|
327
|
+
)
|
|
328
|
+
main_thread_id = thread_id or get_current_thread_id()
|
|
329
|
+
tracker = get_code_history_tracker(main_thread_id)
|
|
330
|
+
if tracker.get_entry_count() > 0:
|
|
331
|
+
enhanced_context = get_context_with_history(context, main_thread_id)
|
|
332
|
+
logger.info(
|
|
333
|
+
f"[TIMING] code history injected "
|
|
334
|
+
f"(entries={tracker.get_entry_count()}, "
|
|
335
|
+
f"context_len={len(enhanced_context) if enhanced_context else 0})"
|
|
336
|
+
)
|
|
337
|
+
except Exception as e:
|
|
338
|
+
logger.warning(f"Failed to inject code history: {e}")
|
|
339
|
+
|
|
340
|
+
# Build message content
|
|
341
|
+
if enhanced_context:
|
|
342
|
+
message_content = f"## Task\n{description}\n\n## Context (provided by Main Agent)\n{enhanced_context}"
|
|
343
|
+
else:
|
|
344
|
+
message_content = description
|
|
345
|
+
|
|
346
|
+
logger.info(f"[{caller_name}] Subagent message length: {len(message_content)}")
|
|
347
|
+
|
|
348
|
+
# Build subagent input state (Deep Agents pattern)
|
|
349
|
+
if runtime_state is not None:
|
|
350
|
+
subagent_state = {
|
|
351
|
+
k: v for k, v in runtime_state.items()
|
|
352
|
+
if k not in _EXCLUDED_STATE_KEYS
|
|
353
|
+
}
|
|
354
|
+
subagent_state["messages"] = [HumanMessage(content=message_content)]
|
|
355
|
+
else:
|
|
356
|
+
subagent_state = {"messages": [{"role": "user", "content": message_content}]}
|
|
357
|
+
|
|
358
|
+
subagent_thread_id = f"subagent-{agent_name}-{uuid.uuid4().hex[:8]}"
|
|
359
|
+
subagent_config = {"configurable": {"thread_id": subagent_thread_id}}
|
|
360
|
+
|
|
361
|
+
t_invoke = time.time()
|
|
362
|
+
logger.info(f"[TIMING] About to invoke subagent '{agent_name}'...")
|
|
363
|
+
result = subagent.invoke(subagent_state, config=subagent_config)
|
|
364
|
+
logger.info(f"[TIMING] subagent.invoke() took {time.time()-t_invoke:.2f}s")
|
|
365
|
+
|
|
366
|
+
# Extract response text
|
|
367
|
+
messages = result.get("messages", [])
|
|
368
|
+
if messages:
|
|
369
|
+
final_message = messages[-1]
|
|
370
|
+
response_text = (
|
|
371
|
+
final_message.content
|
|
372
|
+
if hasattr(final_message, "content")
|
|
373
|
+
else str(final_message)
|
|
374
|
+
)
|
|
375
|
+
else:
|
|
376
|
+
response_text = "Subagent completed but returned no messages."
|
|
377
|
+
|
|
378
|
+
logger.info(
|
|
379
|
+
f"[{caller_name}] Subagent '{agent_name}' returned: {str(response_text)[:200]}..."
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
emit_subagent_complete(agent_name, str(response_text)[:100])
|
|
383
|
+
return result
|
|
384
|
+
|
|
385
|
+
except Exception as e:
|
|
386
|
+
error_msg = f"Subagent '{agent_name}' failed: {str(e)}"
|
|
387
|
+
logger.error(error_msg, exc_info=True)
|
|
388
|
+
emit_subagent_complete(agent_name, f"Error: {str(e)[:50]}")
|
|
389
|
+
raise
|
|
390
|
+
finally:
|
|
391
|
+
clear_current_subagent()
|
|
392
|
+
|
|
393
|
+
|
|
95
394
|
def create_task_tool(
|
|
96
395
|
caller_name: str,
|
|
97
396
|
allowed_subagents: Optional[List[str]] = None,
|
|
98
|
-
):
|
|
99
|
-
"""
|
|
100
|
-
Create a task tool for calling subagents.
|
|
397
|
+
) -> StructuredTool:
|
|
398
|
+
"""Create a task tool for nested subagent calls (returns plain string).
|
|
101
399
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
which the Main Agent can then execute if needed.
|
|
400
|
+
Used by subagents that can call other subagents (e.g., python_developer → athena_query).
|
|
401
|
+
Returns full response text as string (no Command/state extraction).
|
|
105
402
|
|
|
106
403
|
Args:
|
|
107
|
-
caller_name: Name of the agent creating this tool
|
|
108
|
-
allowed_subagents: Optional list of subagent names
|
|
109
|
-
If None, all subagents are allowed (for Main Agent).
|
|
404
|
+
caller_name: Name of the agent creating this tool
|
|
405
|
+
allowed_subagents: Optional list of allowed subagent names
|
|
110
406
|
|
|
111
407
|
Returns:
|
|
112
|
-
|
|
408
|
+
StructuredTool for subagent delegation
|
|
113
409
|
"""
|
|
114
410
|
from agent_server.langchain.subagents.base import (
|
|
115
411
|
SUBAGENT_CONFIGS,
|
|
116
412
|
get_subagent_config,
|
|
117
413
|
)
|
|
118
414
|
|
|
119
|
-
# Build description
|
|
415
|
+
# Build description
|
|
120
416
|
if allowed_subagents:
|
|
121
417
|
available = [
|
|
122
418
|
f"- {name}: {SUBAGENT_CONFIGS[name].description}"
|
|
@@ -124,261 +420,251 @@ def create_task_tool(
|
|
|
124
420
|
if name in SUBAGENT_CONFIGS
|
|
125
421
|
]
|
|
126
422
|
else:
|
|
127
|
-
# Main Agent (planner) can call non-restricted subagents OR those explicitly allowing "planner"
|
|
128
423
|
available = [
|
|
129
424
|
f"- {config.name}: {config.description}"
|
|
130
425
|
for config in SUBAGENT_CONFIGS.values()
|
|
131
426
|
if not config.callable_by or caller_name in config.callable_by
|
|
132
427
|
]
|
|
133
|
-
|
|
134
428
|
available_str = "\n".join(available)
|
|
135
429
|
|
|
136
|
-
# Create Pydantic schema for the task tool (required for Gemini compatibility)
|
|
137
|
-
class TaskInput(BaseModel):
|
|
138
|
-
"""Input schema for task tool"""
|
|
139
|
-
|
|
140
|
-
agent_name: str = Field(
|
|
141
|
-
description=f"Name of the subagent to invoke. Available: {', '.join(allowed_subagents) if allowed_subagents else 'python_developer, researcher, athena_query'}"
|
|
142
|
-
)
|
|
143
|
-
description: str = Field(
|
|
144
|
-
description="Detailed task description for the subagent (Korean preferred)"
|
|
145
|
-
)
|
|
146
|
-
context: Optional[str] = Field(
|
|
147
|
-
default=None,
|
|
148
|
-
description="Additional context for the subagent: resource info (file sizes, memory), previous code, variable state, etc.",
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
@tool(args_schema=TaskInput)
|
|
152
430
|
def task_tool(
|
|
153
|
-
agent_name: str,
|
|
431
|
+
agent_name: str,
|
|
432
|
+
description: str,
|
|
433
|
+
context: Optional[str] = None,
|
|
154
434
|
) -> str:
|
|
155
|
-
"""
|
|
156
|
-
Delegate a task to a specialized subagent.
|
|
157
|
-
|
|
158
|
-
The subagent will execute the task and return its result (code, analysis, etc.).
|
|
159
|
-
Code execution tools (jupyter_cell_tool, write_file_tool) are handled by Main Agent.
|
|
160
|
-
|
|
161
|
-
Args:
|
|
162
|
-
agent_name: Name of the subagent to invoke
|
|
163
|
-
description: Detailed task description for the subagent
|
|
164
|
-
context: Additional context (resource info, previous code, etc.)
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
Result from the subagent execution (string summary or generated code)
|
|
168
|
-
"""
|
|
169
|
-
# Validate subagent exists
|
|
435
|
+
"""Delegate a task to a specialized subagent (nested call)."""
|
|
170
436
|
if agent_name not in SUBAGENT_CONFIGS:
|
|
171
|
-
return f"Error: Unknown agent '{agent_name}'. Available
|
|
437
|
+
return f"Error: Unknown agent '{agent_name}'. Available:\n{available_str}"
|
|
172
438
|
|
|
173
|
-
# Validate caller is allowed to call this subagent
|
|
174
439
|
config = get_subagent_config(agent_name)
|
|
175
440
|
if allowed_subagents and agent_name not in allowed_subagents:
|
|
176
441
|
return f"Error: '{caller_name}' cannot call '{agent_name}'. Allowed: {allowed_subagents}"
|
|
177
|
-
|
|
178
442
|
if config.callable_by and caller_name not in config.callable_by:
|
|
179
443
|
return f"Error: '{agent_name}' can only be called by: {config.callable_by}"
|
|
180
444
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
445
|
+
try:
|
|
446
|
+
result = _invoke_subagent(caller_name, agent_name, description, context)
|
|
447
|
+
messages = result.get("messages", [])
|
|
448
|
+
if messages:
|
|
449
|
+
final = messages[-1]
|
|
450
|
+
return final.content if hasattr(final, "content") else str(final)
|
|
451
|
+
return "Subagent completed but returned no messages."
|
|
452
|
+
except Exception as e:
|
|
453
|
+
return f"Error: Subagent '{agent_name}' failed: {str(e)}"
|
|
184
454
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
set_current_subagent,
|
|
191
|
-
)
|
|
455
|
+
return StructuredTool.from_function(
|
|
456
|
+
name="task_tool",
|
|
457
|
+
func=task_tool,
|
|
458
|
+
description=f"Delegate a task to a specialized subagent.\n\nAvailable agents:\n{available_str}",
|
|
459
|
+
)
|
|
192
460
|
|
|
193
|
-
# Emit subagent start event for UI
|
|
194
|
-
emit_subagent_start(agent_name, description)
|
|
195
461
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
462
|
+
def _create_main_task_tool(
|
|
463
|
+
caller_name: str,
|
|
464
|
+
allowed_subagents: Optional[List[str]] = None,
|
|
465
|
+
) -> StructuredTool:
|
|
466
|
+
"""Create a task tool for Main Agent (returns Command with state update).
|
|
200
467
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
# Set current subagent context for tool call tracking
|
|
205
|
-
set_current_subagent(agent_name)
|
|
206
|
-
|
|
207
|
-
# Get or create the subagent (cached for performance)
|
|
208
|
-
# Avoids expensive LangGraph recompilation on each call
|
|
209
|
-
t0 = time.time()
|
|
210
|
-
subagent = get_or_create_subagent(agent_name, factory_func, llm_config)
|
|
211
|
-
t1 = time.time()
|
|
212
|
-
logger.info(f"[TIMING] get_or_create_subagent took {t1-t0:.2f}s")
|
|
213
|
-
|
|
214
|
-
# Execute subagent synchronously with clean context
|
|
215
|
-
# The subagent runs in isolation, receiving task description + optional context
|
|
216
|
-
import uuid
|
|
217
|
-
|
|
218
|
-
subagent_thread_id = f"subagent-{agent_name}-{uuid.uuid4().hex[:8]}"
|
|
219
|
-
subagent_config = {
|
|
220
|
-
"configurable": {
|
|
221
|
-
"thread_id": subagent_thread_id,
|
|
222
|
-
}
|
|
223
|
-
}
|
|
468
|
+
Uses ToolRuntime for state access and returns Command to update
|
|
469
|
+
generated_content/generated_content_type/content_description in state.
|
|
224
470
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
try:
|
|
229
|
-
t2 = time.time()
|
|
230
|
-
from agent_server.langchain.middleware.code_history_middleware import (
|
|
231
|
-
get_code_history_tracker,
|
|
232
|
-
get_context_with_history,
|
|
233
|
-
)
|
|
471
|
+
Args:
|
|
472
|
+
caller_name: Name of the calling agent (usually "planner")
|
|
473
|
+
allowed_subagents: Optional list of allowed subagent names
|
|
234
474
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
475
|
+
Returns:
|
|
476
|
+
StructuredTool that returns Command
|
|
477
|
+
"""
|
|
478
|
+
# Lazy import to avoid circular dependency
|
|
479
|
+
from langchain.tools import ToolRuntime
|
|
480
|
+
from langgraph.types import Command
|
|
481
|
+
|
|
482
|
+
from agent_server.langchain.subagents.base import (
|
|
483
|
+
SUBAGENT_CONFIGS,
|
|
484
|
+
get_subagent_config,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Build description
|
|
488
|
+
if allowed_subagents:
|
|
489
|
+
available = [
|
|
490
|
+
f"- {name}: {SUBAGENT_CONFIGS[name].description}"
|
|
491
|
+
for name in allowed_subagents
|
|
492
|
+
if name in SUBAGENT_CONFIGS
|
|
493
|
+
]
|
|
494
|
+
else:
|
|
495
|
+
available = [
|
|
496
|
+
f"- {config.name}: {config.description}"
|
|
497
|
+
for config in SUBAGENT_CONFIGS.values()
|
|
498
|
+
if not config.callable_by or caller_name in config.callable_by
|
|
499
|
+
]
|
|
500
|
+
available_str = "\n".join(available)
|
|
246
501
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
502
|
+
def task(
|
|
503
|
+
description: str,
|
|
504
|
+
subagent_type: str,
|
|
505
|
+
runtime: ToolRuntime,
|
|
506
|
+
) -> str:
|
|
507
|
+
"""Delegate a task to a specialized subagent.
|
|
251
508
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
message_content = description
|
|
509
|
+
The subagent executes the task and returns its result.
|
|
510
|
+
For python_developer and athena_query, generated code/SQL is stored
|
|
511
|
+
in state and auto-injected into target tools by ContentInjectionMiddleware.
|
|
256
512
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
513
|
+
Args:
|
|
514
|
+
description: Detailed task description for the subagent
|
|
515
|
+
subagent_type: Name of the subagent to invoke
|
|
516
|
+
runtime: ToolRuntime (auto-injected by LangGraph)
|
|
260
517
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
518
|
+
Returns:
|
|
519
|
+
Command with state update, or error string
|
|
520
|
+
"""
|
|
521
|
+
if subagent_type not in SUBAGENT_CONFIGS:
|
|
522
|
+
return f"Error: Unknown agent '{subagent_type}'. Available:\n{available_str}"
|
|
523
|
+
|
|
524
|
+
config = get_subagent_config(subagent_type)
|
|
525
|
+
if allowed_subagents and subagent_type not in allowed_subagents:
|
|
526
|
+
return f"Error: '{caller_name}' cannot call '{subagent_type}'."
|
|
527
|
+
if config.callable_by and caller_name not in config.callable_by:
|
|
528
|
+
return f"Error: '{subagent_type}' can only be called by: {config.callable_by}"
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
# Extract thread_id from runtime config for code history lookup
|
|
532
|
+
main_thread_id = None
|
|
533
|
+
if hasattr(runtime, "config") and runtime.config:
|
|
534
|
+
configurable = runtime.config.get("configurable", {})
|
|
535
|
+
main_thread_id = configurable.get("thread_id")
|
|
536
|
+
|
|
537
|
+
# Invoke subagent with state sharing (Deep Agents pattern)
|
|
538
|
+
result = _invoke_subagent(
|
|
539
|
+
caller_name,
|
|
540
|
+
subagent_type,
|
|
541
|
+
description,
|
|
542
|
+
context=None,
|
|
543
|
+
runtime_state=dict(runtime.state) if runtime.state else None,
|
|
544
|
+
thread_id=main_thread_id,
|
|
271
545
|
)
|
|
272
546
|
|
|
273
|
-
# Extract
|
|
547
|
+
# Extract response text
|
|
274
548
|
messages = result.get("messages", [])
|
|
549
|
+
response_text = ""
|
|
275
550
|
if messages:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
try:
|
|
291
|
-
from agent_server.langchain.middleware.description_injector import (
|
|
292
|
-
process_task_tool_response,
|
|
293
|
-
)
|
|
551
|
+
final = messages[-1]
|
|
552
|
+
response_text = (
|
|
553
|
+
final.content if hasattr(final, "content") else str(final)
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# Extract generated content to state (Deep Agents Command pattern)
|
|
557
|
+
content, content_type = extract_generated_content(response_text, subagent_type)
|
|
558
|
+
desc = extract_description(response_text)
|
|
559
|
+
|
|
560
|
+
# Build state update (exclude messages/todos like Deep Agents)
|
|
561
|
+
state_update: Dict[str, Any] = {
|
|
562
|
+
k: v for k, v in result.items()
|
|
563
|
+
if k not in _EXCLUDED_STATE_KEYS
|
|
564
|
+
}
|
|
294
565
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
566
|
+
if content:
|
|
567
|
+
state_update["generated_content"] = content
|
|
568
|
+
state_update["generated_content_type"] = content_type
|
|
569
|
+
state_update["content_description"] = desc
|
|
570
|
+
logger.info(
|
|
571
|
+
f"[State] Extracted {content_type} content "
|
|
572
|
+
f"({len(content)} chars) to state"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Build ToolMessage (summary for Main Agent, since content is in state)
|
|
576
|
+
summary = _build_summary(
|
|
577
|
+
response_text,
|
|
578
|
+
subagent_type,
|
|
579
|
+
content_extracted=bool(content),
|
|
580
|
+
)
|
|
298
581
|
|
|
299
|
-
|
|
300
|
-
|
|
582
|
+
if not runtime.tool_call_id:
|
|
583
|
+
raise ValueError("Tool call ID is required for subagent invocation")
|
|
301
584
|
|
|
302
|
-
return
|
|
585
|
+
return Command(
|
|
586
|
+
update={
|
|
587
|
+
**state_update,
|
|
588
|
+
"messages": [
|
|
589
|
+
ToolMessage(
|
|
590
|
+
content=summary,
|
|
591
|
+
tool_call_id=runtime.tool_call_id,
|
|
592
|
+
)
|
|
593
|
+
],
|
|
594
|
+
}
|
|
595
|
+
)
|
|
303
596
|
|
|
304
597
|
except Exception as e:
|
|
305
|
-
error_msg = f"Subagent '{
|
|
598
|
+
error_msg = f"Error: Subagent '{subagent_type}' failed: {str(e)}"
|
|
306
599
|
logger.error(error_msg, exc_info=True)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
- researcher: Returns search results and findings
|
|
323
|
-
- athena_query: Returns SQL query string
|
|
324
|
-
|
|
325
|
-
For code execution (running Python, writing files), use Main Agent's tools directly.
|
|
326
|
-
|
|
327
|
-
IMPORTANT: For python_developer, ALWAYS provide context with:
|
|
328
|
-
- Resource info (file sizes, memory) from check_resource_tool
|
|
329
|
-
- Previous code context if building on existing work
|
|
330
|
-
- Variable names and their current state
|
|
331
|
-
|
|
332
|
-
Args:
|
|
333
|
-
agent_name: Name of the subagent to invoke
|
|
334
|
-
description: Detailed task description for the subagent
|
|
335
|
-
context: Additional context (resource info, previous code, etc.)
|
|
336
|
-
|
|
337
|
-
Returns:
|
|
338
|
-
Result from the subagent execution
|
|
339
|
-
"""
|
|
600
|
+
return error_msg
|
|
601
|
+
|
|
602
|
+
return StructuredTool.from_function(
|
|
603
|
+
name="task_tool",
|
|
604
|
+
func=task,
|
|
605
|
+
description=(
|
|
606
|
+
f"Delegate a task to a specialized subagent.\n\n"
|
|
607
|
+
f"Available agents:\n{available_str}\n\n"
|
|
608
|
+
"Generated code/SQL is automatically injected into execution tools.\n"
|
|
609
|
+
"After calling task_tool, use the appropriate tool to execute/display:\n"
|
|
610
|
+
"- python_developer → jupyter_cell_tool() or write_file_tool()\n"
|
|
611
|
+
"- athena_query → markdown_tool()\n"
|
|
612
|
+
"- researcher → direct response"
|
|
613
|
+
),
|
|
614
|
+
)
|
|
340
615
|
|
|
341
|
-
return task_tool
|
|
342
616
|
|
|
617
|
+
# ---------------------------------------------------------------------------
|
|
618
|
+
# SubAgentMiddleware (extends AgentMiddleware)
|
|
619
|
+
# ---------------------------------------------------------------------------
|
|
343
620
|
|
|
344
621
|
class SubAgentMiddleware:
|
|
345
|
-
"""
|
|
346
|
-
Middleware that adds subagent delegation capability.
|
|
622
|
+
"""Middleware that adds subagent delegation via the task tool.
|
|
347
623
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
624
|
+
Benchmarked from Deep Agents library pattern:
|
|
625
|
+
- Extends AgentMiddleware (tools auto-registered, wrap_model_call hook)
|
|
626
|
+
- Task tool uses ToolRuntime for state access
|
|
627
|
+
- Returns Command for state updates (generated_content in state)
|
|
628
|
+
- ContentInjectionMiddleware handles injection into target tools
|
|
352
629
|
|
|
353
630
|
Usage:
|
|
354
631
|
middleware = SubAgentMiddleware(
|
|
355
|
-
caller_name="
|
|
356
|
-
allowed_subagents=
|
|
632
|
+
caller_name="planner",
|
|
633
|
+
allowed_subagents=None,
|
|
357
634
|
)
|
|
358
635
|
|
|
359
636
|
agent = create_agent(
|
|
360
637
|
model=llm,
|
|
361
638
|
tools=tools,
|
|
362
639
|
middleware=[middleware, ...],
|
|
640
|
+
state_schema=HDSPAgentState,
|
|
363
641
|
)
|
|
364
642
|
"""
|
|
365
643
|
|
|
644
|
+
TASK_SYSTEM_PROMPT = """## task_tool 사용법
|
|
645
|
+
|
|
646
|
+
task_tool로 서브에이전트에게 작업을 위임하세요.
|
|
647
|
+
서브에이전트가 생성한 코드/SQL은 자동으로 실행 도구에 주입됩니다.
|
|
648
|
+
|
|
649
|
+
task_tool 호출 후 처리:
|
|
650
|
+
- python_developer → jupyter_cell_tool() 호출 (코드 자동 주입)
|
|
651
|
+
- python_developer (파일 저장) → write_file_tool() 호출 (코드 자동 주입)
|
|
652
|
+
- athena_query → markdown_tool() 호출 (SQL 자동 주입)
|
|
653
|
+
- researcher → 응답 내용 직접 활용"""
|
|
654
|
+
|
|
366
655
|
def __init__(
|
|
367
656
|
self,
|
|
368
657
|
caller_name: str,
|
|
369
658
|
allowed_subagents: Optional[List[str]] = None,
|
|
370
659
|
):
|
|
371
|
-
"""
|
|
372
|
-
Initialize SubAgentMiddleware.
|
|
373
|
-
|
|
374
|
-
Args:
|
|
375
|
-
caller_name: Name of the agent using this middleware
|
|
376
|
-
allowed_subagents: List of subagents this agent can call.
|
|
377
|
-
None means all non-restricted subagents.
|
|
378
|
-
"""
|
|
379
660
|
self.caller_name = caller_name
|
|
380
661
|
self.allowed_subagents = allowed_subagents
|
|
381
|
-
|
|
662
|
+
|
|
663
|
+
# Create task tool (auto-registered via self.tools)
|
|
664
|
+
self.task_tool = _create_main_task_tool(caller_name, allowed_subagents)
|
|
665
|
+
|
|
666
|
+
# AgentMiddleware convention: tools attribute is auto-registered by create_agent
|
|
667
|
+
self.tools = [self.task_tool]
|
|
382
668
|
|
|
383
669
|
logger.info(
|
|
384
670
|
f"SubAgentMiddleware initialized for '{caller_name}' "
|
|
@@ -386,13 +672,5 @@ class SubAgentMiddleware:
|
|
|
386
672
|
)
|
|
387
673
|
|
|
388
674
|
def get_tools(self) -> List[Any]:
|
|
389
|
-
"""Get the tools provided by this middleware."""
|
|
675
|
+
"""Get the tools provided by this middleware (backward compat)."""
|
|
390
676
|
return [self.task_tool]
|
|
391
|
-
|
|
392
|
-
def __call__(self, tools: List[Any]) -> List[Any]:
|
|
393
|
-
"""
|
|
394
|
-
Add task tool to the agent's toolset.
|
|
395
|
-
|
|
396
|
-
This is called during agent creation to augment the tool list.
|
|
397
|
-
"""
|
|
398
|
-
return tools + [self.task_tool]
|