hdsp-jupyter-extension 2.0.27__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 -31
- agent_server/langchain/custom_middleware.py +268 -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/content_injection_middleware.py +110 -0
- agent_server/langchain/middleware/subagent_events.py +88 -9
- agent_server/langchain/middleware/subagent_middleware.py +501 -245
- 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 +276 -155
- hdsp_agent_core/managers/config_manager.py +100 -1
- {hdsp_jupyter_extension-2.0.27.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.27.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
- hdsp_jupyter_extension-2.0.27.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.27.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4ab73bb5068405670214.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.4ab73bb5068405670214.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.27.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/METADATA +1 -1
- {hdsp_jupyter_extension-2.0.27.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/RECORD +65 -63
- 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.27.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.4ab73bb5068405670214.js → remoteEntry.08fce819ee32e9d25175.js} +3 -3
- hdsp_jupyter_extension-2.0.27.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4ab73bb5068405670214.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.27.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js.map +0 -1
- hdsp_jupyter_extension-2.0.27.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.27.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.27.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.27.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.27.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.27.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.27.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.27.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.27.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.27.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.27.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.27.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.27.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.27.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.27.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.27.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.27.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.27.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.27.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.27.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.27.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.27.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,30 +2,38 @@
|
|
|
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
|
|
|
15
19
|
import contextvars
|
|
16
20
|
import hashlib
|
|
17
21
|
import json
|
|
18
22
|
import logging
|
|
19
|
-
|
|
23
|
+
import re
|
|
24
|
+
import time
|
|
25
|
+
import uuid
|
|
26
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
20
27
|
|
|
21
|
-
from langchain_core.
|
|
28
|
+
from langchain_core.messages import HumanMessage, ToolMessage
|
|
29
|
+
from langchain_core.tools import StructuredTool
|
|
22
30
|
from pydantic import BaseModel, Field
|
|
23
31
|
|
|
24
|
-
if TYPE_CHECKING:
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
32
|
logger = logging.getLogger(__name__)
|
|
28
33
|
|
|
34
|
+
# Deep Agents pattern: state keys excluded from bidirectional sharing
|
|
35
|
+
_EXCLUDED_STATE_KEYS = ("messages", "todos")
|
|
36
|
+
|
|
29
37
|
# Context variable to track the current main agent's thread_id
|
|
30
38
|
_current_thread_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
31
39
|
"current_thread_id", default=None
|
|
@@ -38,15 +46,15 @@ _current_llm_config = None
|
|
|
38
46
|
_subagent_cache: Dict[str, Any] = {}
|
|
39
47
|
|
|
40
48
|
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Global factory management (backward-compatible)
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
41
53
|
def set_subagent_factory(factory_func, llm_config: Dict[str, Any]):
|
|
42
|
-
"""
|
|
43
|
-
Set the subagent factory function.
|
|
44
|
-
Called by AgentFactory during initialization.
|
|
45
|
-
"""
|
|
54
|
+
"""Set the subagent factory function. Called by AgentFactory."""
|
|
46
55
|
global _subagent_factory, _current_llm_config, _subagent_cache
|
|
47
56
|
_subagent_factory = factory_func
|
|
48
57
|
_current_llm_config = llm_config
|
|
49
|
-
# Clear cache when factory changes (new LLM config)
|
|
50
58
|
_subagent_cache.clear()
|
|
51
59
|
logger.info("SubAgentMiddleware factory initialized (cache cleared)")
|
|
52
60
|
|
|
@@ -65,12 +73,7 @@ def _get_config_hash(llm_config: Dict[str, Any]) -> str:
|
|
|
65
73
|
def get_or_create_subagent(
|
|
66
74
|
agent_name: str, factory_func, llm_config: Dict[str, Any]
|
|
67
75
|
) -> Any:
|
|
68
|
-
"""
|
|
69
|
-
Get cached subagent or create new one.
|
|
70
|
-
|
|
71
|
-
Caching avoids expensive recompilation of LangGraph agents.
|
|
72
|
-
Cache key = "{agent_name}_{config_hash}" to handle different LLM configs.
|
|
73
|
-
"""
|
|
76
|
+
"""Get cached subagent or create new one."""
|
|
74
77
|
global _subagent_cache
|
|
75
78
|
|
|
76
79
|
config_hash = _get_config_hash(llm_config)
|
|
@@ -86,18 +89,21 @@ def get_or_create_subagent(
|
|
|
86
89
|
logger.info(
|
|
87
90
|
f"Cached subagent '{agent_name}' (total cached: {len(_subagent_cache)})"
|
|
88
91
|
)
|
|
89
|
-
|
|
90
92
|
return subagent
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
def clear_subagent_cache():
|
|
94
|
-
"""Clear the subagent cache.
|
|
96
|
+
"""Clear the subagent cache."""
|
|
95
97
|
global _subagent_cache
|
|
96
98
|
count = len(_subagent_cache)
|
|
97
99
|
_subagent_cache.clear()
|
|
98
100
|
logger.info(f"Subagent cache cleared ({count} entries removed)")
|
|
99
101
|
|
|
100
102
|
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Thread ID tracking (for code history middleware)
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
101
107
|
def set_current_thread_id(thread_id: str) -> None:
|
|
102
108
|
"""Set the current main agent's thread_id for code history tracking."""
|
|
103
109
|
_current_thread_id.set(thread_id)
|
|
@@ -109,31 +115,304 @@ def get_current_thread_id() -> Optional[str]:
|
|
|
109
115
|
return _current_thread_id.get()
|
|
110
116
|
|
|
111
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
|
+
|
|
112
394
|
def create_task_tool(
|
|
113
395
|
caller_name: str,
|
|
114
396
|
allowed_subagents: Optional[List[str]] = None,
|
|
115
|
-
):
|
|
116
|
-
"""
|
|
117
|
-
Create a task tool for calling subagents.
|
|
397
|
+
) -> StructuredTool:
|
|
398
|
+
"""Create a task tool for nested subagent calls (returns plain string).
|
|
118
399
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
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).
|
|
122
402
|
|
|
123
403
|
Args:
|
|
124
|
-
caller_name: Name of the agent creating this tool
|
|
125
|
-
allowed_subagents: Optional list of subagent names
|
|
126
|
-
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
|
|
127
406
|
|
|
128
407
|
Returns:
|
|
129
|
-
|
|
408
|
+
StructuredTool for subagent delegation
|
|
130
409
|
"""
|
|
131
410
|
from agent_server.langchain.subagents.base import (
|
|
132
411
|
SUBAGENT_CONFIGS,
|
|
133
412
|
get_subagent_config,
|
|
134
413
|
)
|
|
135
414
|
|
|
136
|
-
# Build description
|
|
415
|
+
# Build description
|
|
137
416
|
if allowed_subagents:
|
|
138
417
|
available = [
|
|
139
418
|
f"- {name}: {SUBAGENT_CONFIGS[name].description}"
|
|
@@ -141,266 +420,251 @@ def create_task_tool(
|
|
|
141
420
|
if name in SUBAGENT_CONFIGS
|
|
142
421
|
]
|
|
143
422
|
else:
|
|
144
|
-
# Main Agent (planner) can call non-restricted subagents OR those explicitly allowing "planner"
|
|
145
423
|
available = [
|
|
146
424
|
f"- {config.name}: {config.description}"
|
|
147
425
|
for config in SUBAGENT_CONFIGS.values()
|
|
148
426
|
if not config.callable_by or caller_name in config.callable_by
|
|
149
427
|
]
|
|
150
|
-
|
|
151
428
|
available_str = "\n".join(available)
|
|
152
429
|
|
|
153
|
-
# Create Pydantic schema for the task tool (required for Gemini compatibility)
|
|
154
|
-
class TaskInput(BaseModel):
|
|
155
|
-
"""Input schema for task tool"""
|
|
156
|
-
|
|
157
|
-
agent_name: str = Field(
|
|
158
|
-
description=f"Name of the subagent to invoke. Available: {', '.join(allowed_subagents) if allowed_subagents else 'python_developer, researcher, athena_query'}"
|
|
159
|
-
)
|
|
160
|
-
description: str = Field(
|
|
161
|
-
description="Detailed task description for the subagent (Korean preferred)"
|
|
162
|
-
)
|
|
163
|
-
context: Optional[str] = Field(
|
|
164
|
-
default=None,
|
|
165
|
-
description="Additional context for the subagent: resource info (file sizes, memory), previous code, variable state, etc.",
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
@tool(args_schema=TaskInput)
|
|
169
430
|
def task_tool(
|
|
170
|
-
agent_name: str,
|
|
431
|
+
agent_name: str,
|
|
432
|
+
description: str,
|
|
433
|
+
context: Optional[str] = None,
|
|
171
434
|
) -> str:
|
|
172
|
-
"""
|
|
173
|
-
Delegate a task to a specialized subagent.
|
|
174
|
-
|
|
175
|
-
The subagent will execute the task and return its result (code, analysis, etc.).
|
|
176
|
-
Code execution tools (jupyter_cell_tool, write_file_tool) are handled by Main Agent.
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
agent_name: Name of the subagent to invoke
|
|
180
|
-
description: Detailed task description for the subagent
|
|
181
|
-
context: Additional context (resource info, previous code, etc.)
|
|
182
|
-
|
|
183
|
-
Returns:
|
|
184
|
-
Result from the subagent execution (string summary or generated code)
|
|
185
|
-
"""
|
|
186
|
-
# Validate subagent exists
|
|
435
|
+
"""Delegate a task to a specialized subagent (nested call)."""
|
|
187
436
|
if agent_name not in SUBAGENT_CONFIGS:
|
|
188
|
-
return f"Error: Unknown agent '{agent_name}'. Available
|
|
437
|
+
return f"Error: Unknown agent '{agent_name}'. Available:\n{available_str}"
|
|
189
438
|
|
|
190
|
-
# Validate caller is allowed to call this subagent
|
|
191
439
|
config = get_subagent_config(agent_name)
|
|
192
440
|
if allowed_subagents and agent_name not in allowed_subagents:
|
|
193
441
|
return f"Error: '{caller_name}' cannot call '{agent_name}'. Allowed: {allowed_subagents}"
|
|
194
|
-
|
|
195
442
|
if config.callable_by and caller_name not in config.callable_by:
|
|
196
443
|
return f"Error: '{agent_name}' can only be called by: {config.callable_by}"
|
|
197
444
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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)}"
|
|
201
454
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
set_current_subagent,
|
|
208
|
-
)
|
|
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
|
+
)
|
|
209
460
|
|
|
210
|
-
# Emit subagent start event for UI
|
|
211
|
-
emit_subagent_start(agent_name, description)
|
|
212
461
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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).
|
|
217
467
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
# Set current subagent context for tool call tracking
|
|
222
|
-
set_current_subagent(agent_name)
|
|
223
|
-
|
|
224
|
-
# Get or create the subagent (cached for performance)
|
|
225
|
-
# Avoids expensive LangGraph recompilation on each call
|
|
226
|
-
t0 = time.time()
|
|
227
|
-
subagent = get_or_create_subagent(agent_name, factory_func, llm_config)
|
|
228
|
-
t1 = time.time()
|
|
229
|
-
logger.info(f"[TIMING] get_or_create_subagent took {t1-t0:.2f}s")
|
|
230
|
-
|
|
231
|
-
# Execute subagent synchronously with clean context
|
|
232
|
-
# The subagent runs in isolation, receiving task description + optional context
|
|
233
|
-
import uuid
|
|
234
|
-
|
|
235
|
-
subagent_thread_id = f"subagent-{agent_name}-{uuid.uuid4().hex[:8]}"
|
|
236
|
-
subagent_config = {
|
|
237
|
-
"configurable": {
|
|
238
|
-
"thread_id": subagent_thread_id,
|
|
239
|
-
}
|
|
240
|
-
}
|
|
468
|
+
Uses ToolRuntime for state access and returns Command to update
|
|
469
|
+
generated_content/generated_content_type/content_description in state.
|
|
241
470
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
try:
|
|
246
|
-
t2 = time.time()
|
|
247
|
-
from agent_server.langchain.middleware.code_history_middleware import (
|
|
248
|
-
get_code_history_tracker,
|
|
249
|
-
get_context_with_history,
|
|
250
|
-
)
|
|
471
|
+
Args:
|
|
472
|
+
caller_name: Name of the calling agent (usually "planner")
|
|
473
|
+
allowed_subagents: Optional list of allowed subagent names
|
|
251
474
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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)
|
|
268
501
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
502
|
+
def task(
|
|
503
|
+
description: str,
|
|
504
|
+
subagent_type: str,
|
|
505
|
+
runtime: ToolRuntime,
|
|
506
|
+
) -> str:
|
|
507
|
+
"""Delegate a task to a specialized subagent.
|
|
273
508
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
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.
|
|
278
512
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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)
|
|
282
517
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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,
|
|
293
545
|
)
|
|
294
546
|
|
|
295
|
-
# Extract
|
|
547
|
+
# Extract response text
|
|
296
548
|
messages = result.get("messages", [])
|
|
549
|
+
response_text = ""
|
|
297
550
|
if messages:
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
try:
|
|
313
|
-
from agent_server.langchain.middleware.description_injector import (
|
|
314
|
-
process_task_tool_response,
|
|
315
|
-
)
|
|
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
|
+
}
|
|
316
565
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
)
|
|
320
581
|
|
|
321
|
-
|
|
322
|
-
|
|
582
|
+
if not runtime.tool_call_id:
|
|
583
|
+
raise ValueError("Tool call ID is required for subagent invocation")
|
|
323
584
|
|
|
324
|
-
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
|
+
)
|
|
325
596
|
|
|
326
597
|
except Exception as e:
|
|
327
|
-
error_msg = f"Subagent '{
|
|
598
|
+
error_msg = f"Error: Subagent '{subagent_type}' failed: {str(e)}"
|
|
328
599
|
logger.error(error_msg, exc_info=True)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
- researcher: Returns search results and findings
|
|
345
|
-
- athena_query: Returns SQL query string
|
|
346
|
-
|
|
347
|
-
For code execution (running Python, writing files), use Main Agent's tools directly.
|
|
348
|
-
|
|
349
|
-
IMPORTANT: For python_developer, ALWAYS provide context with:
|
|
350
|
-
- Resource info (file sizes, memory) from check_resource_tool
|
|
351
|
-
- Previous code context if building on existing work
|
|
352
|
-
- Variable names and their current state
|
|
353
|
-
|
|
354
|
-
Args:
|
|
355
|
-
agent_name: Name of the subagent to invoke
|
|
356
|
-
description: Detailed task description for the subagent
|
|
357
|
-
context: Additional context (resource info, previous code, etc.)
|
|
358
|
-
|
|
359
|
-
Returns:
|
|
360
|
-
Result from the subagent execution
|
|
361
|
-
"""
|
|
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
|
+
)
|
|
362
615
|
|
|
363
|
-
return task_tool
|
|
364
616
|
|
|
617
|
+
# ---------------------------------------------------------------------------
|
|
618
|
+
# SubAgentMiddleware (extends AgentMiddleware)
|
|
619
|
+
# ---------------------------------------------------------------------------
|
|
365
620
|
|
|
366
621
|
class SubAgentMiddleware:
|
|
367
|
-
"""
|
|
368
|
-
Middleware that adds subagent delegation capability.
|
|
622
|
+
"""Middleware that adds subagent delegation via the task tool.
|
|
369
623
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
|
374
629
|
|
|
375
630
|
Usage:
|
|
376
631
|
middleware = SubAgentMiddleware(
|
|
377
|
-
caller_name="
|
|
378
|
-
allowed_subagents=
|
|
632
|
+
caller_name="planner",
|
|
633
|
+
allowed_subagents=None,
|
|
379
634
|
)
|
|
380
635
|
|
|
381
636
|
agent = create_agent(
|
|
382
637
|
model=llm,
|
|
383
638
|
tools=tools,
|
|
384
639
|
middleware=[middleware, ...],
|
|
640
|
+
state_schema=HDSPAgentState,
|
|
385
641
|
)
|
|
386
642
|
"""
|
|
387
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
|
+
|
|
388
655
|
def __init__(
|
|
389
656
|
self,
|
|
390
657
|
caller_name: str,
|
|
391
658
|
allowed_subagents: Optional[List[str]] = None,
|
|
392
659
|
):
|
|
393
|
-
"""
|
|
394
|
-
Initialize SubAgentMiddleware.
|
|
395
|
-
|
|
396
|
-
Args:
|
|
397
|
-
caller_name: Name of the agent using this middleware
|
|
398
|
-
allowed_subagents: List of subagents this agent can call.
|
|
399
|
-
None means all non-restricted subagents.
|
|
400
|
-
"""
|
|
401
660
|
self.caller_name = caller_name
|
|
402
661
|
self.allowed_subagents = allowed_subagents
|
|
403
|
-
|
|
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]
|
|
404
668
|
|
|
405
669
|
logger.info(
|
|
406
670
|
f"SubAgentMiddleware initialized for '{caller_name}' "
|
|
@@ -408,13 +672,5 @@ class SubAgentMiddleware:
|
|
|
408
672
|
)
|
|
409
673
|
|
|
410
674
|
def get_tools(self) -> List[Any]:
|
|
411
|
-
"""Get the tools provided by this middleware."""
|
|
675
|
+
"""Get the tools provided by this middleware (backward compat)."""
|
|
412
676
|
return [self.task_tool]
|
|
413
|
-
|
|
414
|
-
def __call__(self, tools: List[Any]) -> List[Any]:
|
|
415
|
-
"""
|
|
416
|
-
Add task tool to the agent's toolset.
|
|
417
|
-
|
|
418
|
-
This is called during agent creation to augment the tool list.
|
|
419
|
-
"""
|
|
420
|
-
return tools + [self.task_tool]
|