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
|
@@ -93,7 +93,6 @@ def _create_vllm_llm(llm_config: Dict[str, Any], callbacks):
|
|
|
93
93
|
from langchain_openai import ChatOpenAI
|
|
94
94
|
|
|
95
95
|
vllm_config = llm_config.get("vllm", {})
|
|
96
|
-
# User provides full base URL (e.g., https://openrouter.ai/api/v1)
|
|
97
96
|
endpoint = vllm_config.get("endpoint", "http://localhost:8000/v1")
|
|
98
97
|
model = vllm_config.get("model", "default")
|
|
99
98
|
api_key = vllm_config.get("apiKey", "dummy")
|
|
@@ -140,9 +139,11 @@ def _create_vllm_llm(llm_config: Dict[str, Any], callbacks):
|
|
|
140
139
|
|
|
141
140
|
|
|
142
141
|
def create_summarization_llm(llm_config: Dict[str, Any]):
|
|
143
|
-
"""Create LLM for summarization middleware.
|
|
142
|
+
"""Create LLM for summarization middleware and /compact feature.
|
|
144
143
|
|
|
145
|
-
|
|
144
|
+
Priority:
|
|
145
|
+
1. If llm_config["summarization"]["enabled"] is True, use that config
|
|
146
|
+
2. Otherwise, fall back to main provider with default summarization model
|
|
146
147
|
|
|
147
148
|
Args:
|
|
148
149
|
llm_config: Configuration dictionary
|
|
@@ -150,60 +151,107 @@ def create_summarization_llm(llm_config: Dict[str, Any]):
|
|
|
150
151
|
Returns:
|
|
151
152
|
LLM instance suitable for summarization, or None if unavailable
|
|
152
153
|
"""
|
|
153
|
-
provider = llm_config.get("provider", "gemini")
|
|
154
|
-
|
|
155
154
|
try:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
167
|
-
elif provider == "openai":
|
|
168
|
-
from langchain_openai import ChatOpenAI
|
|
169
|
-
|
|
170
|
-
openai_config = llm_config.get("openai", {})
|
|
171
|
-
api_key = openai_config.get("apiKey")
|
|
172
|
-
if api_key:
|
|
173
|
-
return ChatOpenAI(
|
|
174
|
-
model="gpt-4o-mini",
|
|
175
|
-
api_key=api_key,
|
|
176
|
-
temperature=0.0,
|
|
177
|
-
)
|
|
178
|
-
elif provider == "vllm":
|
|
179
|
-
vllm_config = llm_config.get("vllm", {})
|
|
180
|
-
# User provides full base URL (e.g., https://openrouter.ai/api/v1)
|
|
181
|
-
endpoint = vllm_config.get("endpoint", "http://localhost:8000/v1")
|
|
182
|
-
model = vllm_config.get("model", "default")
|
|
183
|
-
api_key = vllm_config.get("apiKey", "dummy")
|
|
184
|
-
|
|
185
|
-
# Use ChatGPTOSS for gpt-oss models (but not via OpenRouter)
|
|
186
|
-
is_openrouter = "openrouter" in endpoint.lower()
|
|
187
|
-
if "gpt-oss" in model.lower() and not is_openrouter:
|
|
188
|
-
from agent_server.langchain.models import ChatGPTOSS
|
|
189
|
-
|
|
190
|
-
return ChatGPTOSS(
|
|
191
|
-
model=model,
|
|
192
|
-
base_url=endpoint,
|
|
193
|
-
api_key=api_key,
|
|
194
|
-
temperature=0.0,
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
from langchain_openai import ChatOpenAI
|
|
198
|
-
|
|
199
|
-
return ChatOpenAI(
|
|
200
|
-
model=model,
|
|
201
|
-
api_key=api_key,
|
|
202
|
-
base_url=endpoint, # Use endpoint as-is
|
|
203
|
-
temperature=0.0,
|
|
155
|
+
# 1. Check for dedicated summarization config
|
|
156
|
+
summarization_config = llm_config.get("summarization", {})
|
|
157
|
+
if summarization_config.get("enabled"):
|
|
158
|
+
sum_provider = summarization_config.get("provider", "gemini")
|
|
159
|
+
sum_model = summarization_config.get("model")
|
|
160
|
+
logger.info(
|
|
161
|
+
f"Using dedicated summarization LLM: provider={sum_provider}, model={sum_model or 'default'}"
|
|
162
|
+
)
|
|
163
|
+
return _create_llm_for_provider(
|
|
164
|
+
llm_config, sum_provider, sum_model, for_summarization=True
|
|
204
165
|
)
|
|
166
|
+
|
|
167
|
+
# 2. Fall back to main provider with default summarization model
|
|
168
|
+
provider = llm_config.get("provider", "gemini")
|
|
169
|
+
logger.info(f"Using main provider for summarization: {provider}")
|
|
170
|
+
return _create_llm_for_provider(
|
|
171
|
+
llm_config, provider, None, for_summarization=True
|
|
172
|
+
)
|
|
173
|
+
|
|
205
174
|
except Exception as e:
|
|
206
175
|
logger.warning(f"Failed to create summarization LLM: {e}")
|
|
207
176
|
return None
|
|
208
177
|
|
|
209
|
-
|
|
178
|
+
|
|
179
|
+
def _create_llm_for_provider(
|
|
180
|
+
llm_config: Dict[str, Any],
|
|
181
|
+
provider: str,
|
|
182
|
+
model_override: str = None,
|
|
183
|
+
for_summarization: bool = False,
|
|
184
|
+
):
|
|
185
|
+
"""Create LLM instance for a specific provider.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
llm_config: Full configuration dictionary (for credentials)
|
|
189
|
+
provider: Provider to use ('gemini', 'openai', 'vllm')
|
|
190
|
+
model_override: Optional model name override
|
|
191
|
+
for_summarization: If True, use lightweight default models
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
LLM instance or None
|
|
195
|
+
"""
|
|
196
|
+
if provider == "gemini":
|
|
197
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
198
|
+
|
|
199
|
+
gemini_config = llm_config.get("gemini", {})
|
|
200
|
+
api_key = gemini_config.get("apiKey")
|
|
201
|
+
if not api_key:
|
|
202
|
+
logger.warning("No Gemini API key found")
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
model = model_override or ("gemini-2.5-flash" if for_summarization else gemini_config.get("model", "gemini-2.5-flash"))
|
|
206
|
+
return ChatGoogleGenerativeAI(
|
|
207
|
+
model=model,
|
|
208
|
+
google_api_key=api_key,
|
|
209
|
+
temperature=0.0,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
elif provider == "openai":
|
|
213
|
+
from langchain_openai import ChatOpenAI
|
|
214
|
+
|
|
215
|
+
openai_config = llm_config.get("openai", {})
|
|
216
|
+
api_key = openai_config.get("apiKey")
|
|
217
|
+
if not api_key:
|
|
218
|
+
logger.warning("No OpenAI API key found")
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
model = model_override or ("gpt-4o-mini" if for_summarization else openai_config.get("model", "gpt-4"))
|
|
222
|
+
return ChatOpenAI(
|
|
223
|
+
model=model,
|
|
224
|
+
api_key=api_key,
|
|
225
|
+
temperature=0.0,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
elif provider == "vllm":
|
|
229
|
+
vllm_config = llm_config.get("vllm", {})
|
|
230
|
+
endpoint = vllm_config.get("endpoint", "http://localhost:8000/v1")
|
|
231
|
+
api_key = vllm_config.get("apiKey", "dummy")
|
|
232
|
+
model = model_override or vllm_config.get("model", "default")
|
|
233
|
+
|
|
234
|
+
# Use ChatGPTOSS for gpt-oss models (but not via OpenRouter)
|
|
235
|
+
is_openrouter = "openrouter" in endpoint.lower()
|
|
236
|
+
if "gpt-oss" in model.lower() and not is_openrouter:
|
|
237
|
+
from agent_server.langchain.models import ChatGPTOSS
|
|
238
|
+
|
|
239
|
+
return ChatGPTOSS(
|
|
240
|
+
model=model,
|
|
241
|
+
base_url=endpoint,
|
|
242
|
+
api_key=api_key,
|
|
243
|
+
temperature=0.0,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
from langchain_openai import ChatOpenAI
|
|
247
|
+
|
|
248
|
+
return ChatOpenAI(
|
|
249
|
+
model=model,
|
|
250
|
+
api_key=api_key,
|
|
251
|
+
base_url=endpoint,
|
|
252
|
+
temperature=0.0,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
else:
|
|
256
|
+
logger.warning(f"Unknown provider: {provider}")
|
|
257
|
+
return None
|
|
@@ -37,7 +37,7 @@ def disable_langchain_logging():
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
# Auto-disable on import (comment this line to re-enable all logs)
|
|
40
|
-
disable_langchain_logging()
|
|
40
|
+
# disable_langchain_logging() # TEMPORARILY ENABLED FOR DEBUGGING
|
|
41
41
|
|
|
42
42
|
LOG_SEPARATOR = "=" * 96
|
|
43
43
|
LOG_SUBSECTION = "-" * 96
|
|
@@ -3,10 +3,14 @@ Middleware Module
|
|
|
3
3
|
|
|
4
4
|
Custom middleware for the multi-agent architecture:
|
|
5
5
|
- SubAgentMiddleware: Handles subagent delegation via task tool
|
|
6
|
+
- ContentInjectionMiddleware: Injects generated code/SQL into tool args
|
|
6
7
|
- SkillMiddleware: Progressive skill loading for code generation agents
|
|
7
8
|
- Existing middleware from custom_middleware.py is also available
|
|
8
9
|
"""
|
|
9
10
|
|
|
11
|
+
from agent_server.langchain.middleware.content_injection_middleware import (
|
|
12
|
+
ContentInjectionMiddleware,
|
|
13
|
+
)
|
|
10
14
|
from agent_server.langchain.middleware.skill_middleware import (
|
|
11
15
|
SkillMiddleware,
|
|
12
16
|
get_skill_middleware,
|
|
@@ -18,6 +22,7 @@ from agent_server.langchain.middleware.subagent_middleware import (
|
|
|
18
22
|
|
|
19
23
|
__all__ = [
|
|
20
24
|
"SubAgentMiddleware",
|
|
25
|
+
"ContentInjectionMiddleware",
|
|
21
26
|
"create_task_tool",
|
|
22
27
|
"SkillMiddleware",
|
|
23
28
|
"get_skill_middleware",
|
|
@@ -12,11 +12,12 @@ Features:
|
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
14
|
import threading
|
|
15
|
-
import tiktoken
|
|
16
15
|
from dataclasses import dataclass, field
|
|
17
16
|
from datetime import datetime
|
|
18
17
|
from typing import Any, Dict, List, Optional
|
|
19
18
|
|
|
19
|
+
import tiktoken
|
|
20
|
+
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
22
23
|
# Token limit for context (including system prompt)
|
|
@@ -31,7 +32,9 @@ PYTHON_DEV_SYSTEM_PROMPT_TOKENS = 2000
|
|
|
31
32
|
class CodeHistoryEntry:
|
|
32
33
|
"""Represents a single code execution or file operation."""
|
|
33
34
|
|
|
34
|
-
tool_name:
|
|
35
|
+
tool_name: (
|
|
36
|
+
str # jupyter_cell_tool, write_file_tool, edit_file_tool, multiedit_file_tool
|
|
37
|
+
)
|
|
35
38
|
timestamp: datetime = field(default_factory=datetime.now)
|
|
36
39
|
|
|
37
40
|
# For jupyter_cell_tool
|
|
@@ -50,7 +53,9 @@ class CodeHistoryEntry:
|
|
|
50
53
|
timestamp_str = self.timestamp.strftime("%H:%M:%S")
|
|
51
54
|
|
|
52
55
|
if self.tool_name == "jupyter_cell_tool":
|
|
53
|
-
output_preview =
|
|
56
|
+
output_preview = (
|
|
57
|
+
self._truncate(self.output, 500) if self.output else "(no output)"
|
|
58
|
+
)
|
|
54
59
|
return f"""## Cell ({timestamp_str})
|
|
55
60
|
```python
|
|
56
61
|
{self.code}
|
|
@@ -86,7 +91,7 @@ Changes: {edit_count} edits applied"""
|
|
|
86
91
|
if self.tool_name == "jupyter_cell_tool":
|
|
87
92
|
# Extract first meaningful line of code
|
|
88
93
|
if self.code:
|
|
89
|
-
first_line = self.code.strip().split(
|
|
94
|
+
first_line = self.code.strip().split("\n")[0][:60]
|
|
90
95
|
return f"- Cell: {first_line}..."
|
|
91
96
|
return "- Cell: (empty)"
|
|
92
97
|
|
|
@@ -156,7 +161,9 @@ class CodeHistoryTracker:
|
|
|
156
161
|
output=output,
|
|
157
162
|
)
|
|
158
163
|
self._history.append(entry)
|
|
159
|
-
logger.info(
|
|
164
|
+
logger.info(
|
|
165
|
+
f"CodeHistory: Added jupyter_cell (total: {len(self._history)})"
|
|
166
|
+
)
|
|
160
167
|
|
|
161
168
|
def add_write_file(self, file_path: str, content: str) -> None:
|
|
162
169
|
"""Track a write_file_tool execution."""
|
|
@@ -167,7 +174,9 @@ class CodeHistoryTracker:
|
|
|
167
174
|
content=content,
|
|
168
175
|
)
|
|
169
176
|
self._history.append(entry)
|
|
170
|
-
logger.info(
|
|
177
|
+
logger.info(
|
|
178
|
+
f"CodeHistory: Added write_file {file_path} (total: {len(self._history)})"
|
|
179
|
+
)
|
|
171
180
|
|
|
172
181
|
def add_edit_file(self, file_path: str, old_content: str, new_content: str) -> None:
|
|
173
182
|
"""Track an edit_file_tool execution."""
|
|
@@ -179,7 +188,9 @@ class CodeHistoryTracker:
|
|
|
179
188
|
new_content=new_content,
|
|
180
189
|
)
|
|
181
190
|
self._history.append(entry)
|
|
182
|
-
logger.info(
|
|
191
|
+
logger.info(
|
|
192
|
+
f"CodeHistory: Added edit_file {file_path} (total: {len(self._history)})"
|
|
193
|
+
)
|
|
183
194
|
|
|
184
195
|
def add_multiedit_file(self, file_path: str, edits: List[Dict[str, str]]) -> None:
|
|
185
196
|
"""Track a multiedit_file_tool execution."""
|
|
@@ -190,7 +201,9 @@ class CodeHistoryTracker:
|
|
|
190
201
|
edits=edits,
|
|
191
202
|
)
|
|
192
203
|
self._history.append(entry)
|
|
193
|
-
logger.info(
|
|
204
|
+
logger.info(
|
|
205
|
+
f"CodeHistory: Added multiedit_file {file_path} (total: {len(self._history)})"
|
|
206
|
+
)
|
|
194
207
|
|
|
195
208
|
def get_context_for_subagent(
|
|
196
209
|
self,
|
|
@@ -216,8 +229,12 @@ class CodeHistoryTracker:
|
|
|
216
229
|
return existing_context or ""
|
|
217
230
|
|
|
218
231
|
# Calculate available tokens for history
|
|
219
|
-
existing_tokens =
|
|
220
|
-
|
|
232
|
+
existing_tokens = (
|
|
233
|
+
self._count_tokens(existing_context) if existing_context else 0
|
|
234
|
+
)
|
|
235
|
+
available_tokens = (
|
|
236
|
+
max_tokens - system_prompt_tokens - existing_tokens - 500
|
|
237
|
+
) # 500 buffer
|
|
221
238
|
|
|
222
239
|
# Build full history string
|
|
223
240
|
full_history = self._build_full_history()
|
|
@@ -311,49 +328,110 @@ class CodeHistoryTracker:
|
|
|
311
328
|
return len(self._history)
|
|
312
329
|
|
|
313
330
|
|
|
314
|
-
# Global tracker
|
|
315
|
-
|
|
331
|
+
# Global tracker instances per threadId
|
|
332
|
+
_code_history_trackers: Dict[str, CodeHistoryTracker] = {}
|
|
333
|
+
_trackers_lock = threading.Lock()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def get_code_history_tracker(thread_id: Optional[str] = None) -> CodeHistoryTracker:
|
|
337
|
+
"""
|
|
338
|
+
Get the CodeHistoryTracker instance for the given thread_id.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
thread_id: Thread ID for session isolation. If None, returns a temporary tracker.
|
|
316
342
|
|
|
343
|
+
Returns:
|
|
344
|
+
CodeHistoryTracker instance for the thread
|
|
345
|
+
"""
|
|
346
|
+
if thread_id is None:
|
|
347
|
+
logger.warning(
|
|
348
|
+
"get_code_history_tracker called without thread_id - using temporary tracker"
|
|
349
|
+
)
|
|
350
|
+
return CodeHistoryTracker()
|
|
317
351
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return _code_history_tracker
|
|
352
|
+
with _trackers_lock:
|
|
353
|
+
if thread_id not in _code_history_trackers:
|
|
354
|
+
_code_history_trackers[thread_id] = CodeHistoryTracker()
|
|
355
|
+
logger.info(f"CodeHistory: Created new tracker for thread_id={thread_id}")
|
|
356
|
+
return _code_history_trackers[thread_id]
|
|
324
357
|
|
|
325
358
|
|
|
326
|
-
def track_jupyter_cell(code: str, output: str) -> None:
|
|
359
|
+
def track_jupyter_cell(code: str, output: str, thread_id: Optional[str] = None) -> None:
|
|
327
360
|
"""Convenience function to track jupyter_cell_tool execution."""
|
|
328
|
-
get_code_history_tracker().add_jupyter_cell(code, output)
|
|
361
|
+
get_code_history_tracker(thread_id).add_jupyter_cell(code, output)
|
|
329
362
|
|
|
330
363
|
|
|
331
|
-
def track_write_file(
|
|
364
|
+
def track_write_file(
|
|
365
|
+
file_path: str, content: str, thread_id: Optional[str] = None
|
|
366
|
+
) -> None:
|
|
332
367
|
"""Convenience function to track write_file_tool execution."""
|
|
333
|
-
get_code_history_tracker().add_write_file(file_path, content)
|
|
368
|
+
get_code_history_tracker(thread_id).add_write_file(file_path, content)
|
|
334
369
|
|
|
335
370
|
|
|
336
|
-
def track_edit_file(
|
|
371
|
+
def track_edit_file(
|
|
372
|
+
file_path: str,
|
|
373
|
+
old_content: str,
|
|
374
|
+
new_content: str,
|
|
375
|
+
thread_id: Optional[str] = None,
|
|
376
|
+
) -> None:
|
|
337
377
|
"""Convenience function to track edit_file_tool execution."""
|
|
338
|
-
get_code_history_tracker().add_edit_file(
|
|
378
|
+
get_code_history_tracker(thread_id).add_edit_file(
|
|
379
|
+
file_path, old_content, new_content
|
|
380
|
+
)
|
|
339
381
|
|
|
340
382
|
|
|
341
|
-
def track_multiedit_file(
|
|
383
|
+
def track_multiedit_file(
|
|
384
|
+
file_path: str,
|
|
385
|
+
edits: List[Dict[str, str]],
|
|
386
|
+
thread_id: Optional[str] = None,
|
|
387
|
+
) -> None:
|
|
342
388
|
"""Convenience function to track multiedit_file_tool execution."""
|
|
343
|
-
get_code_history_tracker().add_multiedit_file(file_path, edits)
|
|
389
|
+
get_code_history_tracker(thread_id).add_multiedit_file(file_path, edits)
|
|
344
390
|
|
|
345
391
|
|
|
346
|
-
def get_context_with_history(
|
|
392
|
+
def get_context_with_history(
|
|
393
|
+
existing_context: Optional[str] = None,
|
|
394
|
+
thread_id: Optional[str] = None,
|
|
395
|
+
) -> str:
|
|
347
396
|
"""Get context string with code history injected."""
|
|
348
|
-
return get_code_history_tracker().get_context_for_subagent(
|
|
397
|
+
return get_code_history_tracker(thread_id).get_context_for_subagent(
|
|
398
|
+
existing_context
|
|
399
|
+
)
|
|
400
|
+
|
|
349
401
|
|
|
402
|
+
def clear_code_history(thread_id: Optional[str] = None) -> None:
|
|
403
|
+
"""
|
|
404
|
+
Clear code history for a specific thread or all threads.
|
|
350
405
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
406
|
+
Args:
|
|
407
|
+
thread_id: Thread ID to clear. If None, clears all threads.
|
|
408
|
+
"""
|
|
409
|
+
if thread_id is None:
|
|
410
|
+
# Clear all trackers
|
|
411
|
+
with _trackers_lock:
|
|
412
|
+
for tid, tracker in _code_history_trackers.items():
|
|
413
|
+
tracker.clear()
|
|
414
|
+
logger.info(f"CodeHistory: Cleared history for thread_id={tid}")
|
|
415
|
+
_code_history_trackers.clear()
|
|
416
|
+
logger.info("CodeHistory: Cleared all thread trackers")
|
|
417
|
+
else:
|
|
418
|
+
# Clear specific thread
|
|
419
|
+
with _trackers_lock:
|
|
420
|
+
if thread_id in _code_history_trackers:
|
|
421
|
+
_code_history_trackers[thread_id].clear()
|
|
422
|
+
del _code_history_trackers[thread_id]
|
|
423
|
+
logger.info(
|
|
424
|
+
f"CodeHistory: Cleared and removed tracker for thread_id={thread_id}"
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
logger.info(f"CodeHistory: No tracker found for thread_id={thread_id}")
|
|
354
428
|
|
|
355
429
|
|
|
356
|
-
def track_tool_execution(
|
|
430
|
+
def track_tool_execution(
|
|
431
|
+
tool_name: str,
|
|
432
|
+
args: Dict[str, Any],
|
|
433
|
+
thread_id: Optional[str] = None,
|
|
434
|
+
) -> None:
|
|
357
435
|
"""
|
|
358
436
|
Track a tool execution from HITL decision processing.
|
|
359
437
|
|
|
@@ -363,6 +441,7 @@ def track_tool_execution(tool_name: str, args: Dict[str, Any]) -> None:
|
|
|
363
441
|
Args:
|
|
364
442
|
tool_name: Name of the tool (jupyter_cell_tool, write_file_tool, etc.)
|
|
365
443
|
args: Tool arguments including execution_result
|
|
444
|
+
thread_id: Thread ID for session isolation
|
|
366
445
|
"""
|
|
367
446
|
if not args:
|
|
368
447
|
return
|
|
@@ -371,21 +450,26 @@ def track_tool_execution(tool_name: str, args: Dict[str, Any]) -> None:
|
|
|
371
450
|
if not execution_result:
|
|
372
451
|
return
|
|
373
452
|
|
|
374
|
-
tracker = get_code_history_tracker()
|
|
453
|
+
tracker = get_code_history_tracker(thread_id)
|
|
375
454
|
|
|
376
455
|
if tool_name == "jupyter_cell_tool":
|
|
377
456
|
code = args.get("code", "")
|
|
378
457
|
output = execution_result.get("output", "")
|
|
379
458
|
if code:
|
|
380
459
|
tracker.add_jupyter_cell(code, output)
|
|
381
|
-
logger.info(
|
|
460
|
+
logger.info(
|
|
461
|
+
f"CodeHistory: Tracked jupyter_cell execution "
|
|
462
|
+
f"(code len={len(code)}, thread_id={thread_id})"
|
|
463
|
+
)
|
|
382
464
|
|
|
383
465
|
elif tool_name == "write_file_tool":
|
|
384
466
|
file_path = args.get("path", "")
|
|
385
467
|
content = args.get("content", "")
|
|
386
468
|
if file_path:
|
|
387
469
|
tracker.add_write_file(file_path, content)
|
|
388
|
-
logger.info(
|
|
470
|
+
logger.info(
|
|
471
|
+
f"CodeHistory: Tracked write_file to {file_path} (thread_id={thread_id})"
|
|
472
|
+
)
|
|
389
473
|
|
|
390
474
|
elif tool_name == "edit_file_tool":
|
|
391
475
|
file_path = args.get("path", "")
|
|
@@ -393,7 +477,9 @@ def track_tool_execution(tool_name: str, args: Dict[str, Any]) -> None:
|
|
|
393
477
|
new_string = args.get("new_string", "")
|
|
394
478
|
if file_path:
|
|
395
479
|
tracker.add_edit_file(file_path, old_string, new_string)
|
|
396
|
-
logger.info(
|
|
480
|
+
logger.info(
|
|
481
|
+
f"CodeHistory: Tracked edit_file to {file_path} (thread_id={thread_id})"
|
|
482
|
+
)
|
|
397
483
|
|
|
398
484
|
elif tool_name == "multiedit_file_tool":
|
|
399
485
|
file_path = args.get("path", "")
|
|
@@ -409,4 +495,7 @@ def track_tool_execution(tool_name: str, args: Dict[str, Any]) -> None:
|
|
|
409
495
|
elif isinstance(edit, dict):
|
|
410
496
|
edits_as_dicts.append(edit)
|
|
411
497
|
tracker.add_multiedit_file(file_path, edits_as_dicts)
|
|
412
|
-
logger.info(
|
|
498
|
+
logger.info(
|
|
499
|
+
f"CodeHistory: Tracked multiedit_file to {file_path} "
|
|
500
|
+
f"({len(edits)} edits, thread_id={thread_id})"
|
|
501
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ContentInjectionMiddleware
|
|
3
|
+
|
|
4
|
+
Injects generated_content from LangGraph state into target tool args.
|
|
5
|
+
This eliminates JSON escaping issues when passing code/SQL between agents.
|
|
6
|
+
|
|
7
|
+
Runs BEFORE HumanInTheLoopMiddleware so HITL shows the full injected content.
|
|
8
|
+
|
|
9
|
+
Flow:
|
|
10
|
+
1. Subagent generates code/SQL → stored in state via Command
|
|
11
|
+
2. Main Agent calls target tool (e.g., jupyter_cell_tool) without args
|
|
12
|
+
3. This middleware reads state and injects content into tool args
|
|
13
|
+
4. HITL middleware sees full content for user approval
|
|
14
|
+
|
|
15
|
+
content_type → tool injection mapping:
|
|
16
|
+
- "python" → jupyter_cell_tool(code=...), write_file_tool(content=...)
|
|
17
|
+
- "sql" → markdown_tool(content="```sql\\n...\\n```")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from typing import Any, Callable, Union
|
|
22
|
+
|
|
23
|
+
from langchain.agents.middleware import AgentMiddleware
|
|
24
|
+
from langchain_core.messages import ToolMessage
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ContentInjectionMiddleware(AgentMiddleware):
|
|
30
|
+
"""Inject state's generated_content into target tool call args.
|
|
31
|
+
|
|
32
|
+
When a subagent generates code/SQL via task_tool, it's stored in
|
|
33
|
+
LangGraph state (generated_content, generated_content_type, content_description).
|
|
34
|
+
This middleware reads the state and injects the content into the
|
|
35
|
+
appropriate tool's arguments before execution.
|
|
36
|
+
|
|
37
|
+
This ensures:
|
|
38
|
+
1. Code/SQL bypasses LLM JSON serialization (no escaping issues)
|
|
39
|
+
2. HITL middleware sees the full injected content for approval
|
|
40
|
+
3. Main Agent doesn't need to copy code into tool args
|
|
41
|
+
|
|
42
|
+
Usage in agent_factory.py:
|
|
43
|
+
middleware = [
|
|
44
|
+
ContentInjectionMiddleware(), # BEFORE HITL
|
|
45
|
+
...,
|
|
46
|
+
hitl_middleware, # Sees injected content
|
|
47
|
+
]
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def wrap_tool_call(self, request, handler):
|
|
51
|
+
"""Intercept tool calls and inject generated content from state.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
request: ToolCallRequest with tool_call, state, runtime
|
|
55
|
+
handler: Next handler in middleware chain
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
ToolMessage or Command from handler
|
|
59
|
+
"""
|
|
60
|
+
state = request.state
|
|
61
|
+
if not state:
|
|
62
|
+
return handler(request)
|
|
63
|
+
|
|
64
|
+
content = state.get("generated_content") if isinstance(state, dict) else getattr(state, "generated_content", None)
|
|
65
|
+
content_type = state.get("generated_content_type") if isinstance(state, dict) else getattr(state, "generated_content_type", None)
|
|
66
|
+
desc = state.get("content_description") if isinstance(state, dict) else getattr(state, "content_description", None)
|
|
67
|
+
|
|
68
|
+
if not content or not content_type:
|
|
69
|
+
return handler(request)
|
|
70
|
+
|
|
71
|
+
tool_call = request.tool_call
|
|
72
|
+
tool_name = tool_call["name"]
|
|
73
|
+
args = tool_call.get("args", {})
|
|
74
|
+
|
|
75
|
+
new_args = None
|
|
76
|
+
|
|
77
|
+
# Python code injection
|
|
78
|
+
if content_type == "python":
|
|
79
|
+
if tool_name == "jupyter_cell_tool" and not args.get("code"):
|
|
80
|
+
new_args = {**args, "code": content}
|
|
81
|
+
if desc and not args.get("description"):
|
|
82
|
+
new_args["description"] = desc
|
|
83
|
+
logger.info(
|
|
84
|
+
"[ContentInjection] Injected python code (%d chars) into jupyter_cell_tool",
|
|
85
|
+
len(content),
|
|
86
|
+
)
|
|
87
|
+
elif tool_name == "write_file_tool" and not args.get("content"):
|
|
88
|
+
new_args = {**args, "content": content}
|
|
89
|
+
logger.info(
|
|
90
|
+
"[ContentInjection] Injected python code (%d chars) into write_file_tool",
|
|
91
|
+
len(content),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# SQL query injection
|
|
95
|
+
elif content_type == "sql":
|
|
96
|
+
if tool_name == "markdown_tool" and not args.get("content"):
|
|
97
|
+
sql_markdown = f"```sql\n{content}\n```"
|
|
98
|
+
if desc:
|
|
99
|
+
sql_markdown = f"{desc}\n\n{sql_markdown}"
|
|
100
|
+
new_args = {**args, "content": sql_markdown}
|
|
101
|
+
logger.info(
|
|
102
|
+
"[ContentInjection] Injected SQL (%d chars) into markdown_tool",
|
|
103
|
+
len(content),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if new_args is not None:
|
|
107
|
+
modified_call = {**tool_call, "args": new_args}
|
|
108
|
+
request = request.override(tool_call=modified_call)
|
|
109
|
+
|
|
110
|
+
return handler(request)
|