hdsp-jupyter-extension 2.0.10__py3-none-any.whl → 2.0.13__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/core/notebook_generator.py +4 -4
- agent_server/langchain/MULTI_AGENT_ARCHITECTURE.md +1114 -0
- agent_server/langchain/__init__.py +2 -2
- agent_server/langchain/agent.py +72 -33
- agent_server/langchain/agent_factory.py +400 -0
- agent_server/langchain/agent_prompts/__init__.py +25 -0
- agent_server/langchain/agent_prompts/athena_query_prompt.py +71 -0
- agent_server/langchain/agent_prompts/planner_prompt.py +85 -0
- agent_server/langchain/agent_prompts/python_developer_prompt.py +123 -0
- agent_server/langchain/agent_prompts/researcher_prompt.py +38 -0
- agent_server/langchain/custom_middleware.py +656 -113
- agent_server/langchain/hitl_config.py +38 -9
- agent_server/langchain/llm_factory.py +1 -85
- agent_server/langchain/middleware/__init__.py +24 -0
- agent_server/langchain/middleware/code_history_middleware.py +412 -0
- agent_server/langchain/middleware/description_injector.py +150 -0
- agent_server/langchain/middleware/skill_middleware.py +298 -0
- agent_server/langchain/middleware/subagent_events.py +171 -0
- agent_server/langchain/middleware/subagent_middleware.py +329 -0
- agent_server/langchain/prompts.py +107 -135
- agent_server/langchain/skills/data_analysis.md +236 -0
- agent_server/langchain/skills/data_loading.md +158 -0
- agent_server/langchain/skills/inference.md +392 -0
- agent_server/langchain/skills/model_training.md +318 -0
- agent_server/langchain/skills/pyspark.md +352 -0
- agent_server/langchain/subagents/__init__.py +20 -0
- agent_server/langchain/subagents/base.py +173 -0
- agent_server/langchain/tools/__init__.py +3 -0
- agent_server/langchain/tools/jupyter_tools.py +58 -20
- agent_server/langchain/tools/lsp_tools.py +1 -1
- agent_server/langchain/tools/shared/__init__.py +26 -0
- agent_server/langchain/tools/shared/qdrant_search.py +175 -0
- agent_server/langchain/tools/tool_registry.py +219 -0
- agent_server/langchain/tools/workspace_tools.py +197 -0
- agent_server/prompts/file_action_prompts.py +8 -8
- agent_server/routers/config.py +40 -1
- agent_server/routers/langchain_agent.py +868 -321
- hdsp_agent_core/__init__.py +46 -47
- hdsp_agent_core/factory.py +6 -10
- hdsp_agent_core/interfaces.py +4 -2
- hdsp_agent_core/knowledge/__init__.py +5 -5
- hdsp_agent_core/knowledge/chunking.py +87 -61
- hdsp_agent_core/knowledge/loader.py +103 -101
- hdsp_agent_core/llm/service.py +192 -107
- hdsp_agent_core/managers/config_manager.py +16 -22
- hdsp_agent_core/managers/session_manager.py +5 -4
- hdsp_agent_core/models/__init__.py +12 -12
- hdsp_agent_core/models/agent.py +15 -8
- hdsp_agent_core/models/common.py +1 -2
- hdsp_agent_core/models/rag.py +48 -111
- hdsp_agent_core/prompts/__init__.py +12 -12
- hdsp_agent_core/prompts/cell_action_prompts.py +9 -7
- hdsp_agent_core/services/agent_service.py +10 -8
- hdsp_agent_core/services/chat_service.py +10 -6
- hdsp_agent_core/services/rag_service.py +3 -6
- hdsp_agent_core/tests/conftest.py +4 -1
- hdsp_agent_core/tests/test_factory.py +2 -2
- hdsp_agent_core/tests/test_services.py +12 -19
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.13.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.13.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +7 -2
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js → hdsp_jupyter_extension-2.0.13.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.037b3c8e5d6a92b63b16.js +1108 -179
- hdsp_jupyter_extension-2.0.13.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.037b3c8e5d6a92b63b16.js.map +1 -0
- jupyter_ext/labextension/static/lib_index_js.dc6434bee96ab03a0539.js → hdsp_jupyter_extension-2.0.13.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.5449ba3c7e25177d2987.js +3936 -8144
- hdsp_jupyter_extension-2.0.13.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.5449ba3c7e25177d2987.js.map +1 -0
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4a252df3ade74efee8d6.js → hdsp_jupyter_extension-2.0.13.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.a8e0b064eb9b1c1ff463.js +17 -17
- hdsp_jupyter_extension-2.0.13.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.a8e0b064eb9b1c1ff463.js.map +1 -0
- {hdsp_jupyter_extension-2.0.10.dist-info → hdsp_jupyter_extension-2.0.13.dist-info}/METADATA +1 -1
- {hdsp_jupyter_extension-2.0.10.dist-info → hdsp_jupyter_extension-2.0.13.dist-info}/RECORD +100 -76
- jupyter_ext/__init__.py +21 -11
- jupyter_ext/_version.py +1 -1
- jupyter_ext/handlers.py +128 -58
- jupyter_ext/labextension/build_log.json +1 -1
- jupyter_ext/labextension/package.json +7 -2
- jupyter_ext/labextension/static/{frontend_styles_index_js.2d9fb488c82498c45c2d.js → frontend_styles_index_js.037b3c8e5d6a92b63b16.js} +1108 -179
- jupyter_ext/labextension/static/frontend_styles_index_js.037b3c8e5d6a92b63b16.js.map +1 -0
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js → jupyter_ext/labextension/static/lib_index_js.5449ba3c7e25177d2987.js +3936 -8144
- jupyter_ext/labextension/static/lib_index_js.5449ba3c7e25177d2987.js.map +1 -0
- jupyter_ext/labextension/static/{remoteEntry.4a252df3ade74efee8d6.js → remoteEntry.a8e0b064eb9b1c1ff463.js} +17 -17
- jupyter_ext/labextension/static/remoteEntry.a8e0b064eb9b1c1ff463.js.map +1 -0
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +0 -1
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js.map +0 -1
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4a252df3ade74efee8d6.js.map +0 -1
- jupyter_ext/labextension/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +0 -1
- jupyter_ext/labextension/static/lib_index_js.dc6434bee96ab03a0539.js.map +0 -1
- jupyter_ext/labextension/static/remoteEntry.4a252df3ade74efee8d6.js.map +0 -1
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.13.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.13.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.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.10.data → hdsp_jupyter_extension-2.0.13.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.10.dist-info → hdsp_jupyter_extension-2.0.13.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.10.dist-info → hdsp_jupyter_extension-2.0.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -24,40 +24,69 @@ def get_hitl_interrupt_config() -> Dict[str, Any]:
|
|
|
24
24
|
# Require approval before executing code
|
|
25
25
|
"jupyter_cell_tool": {
|
|
26
26
|
"allowed_decisions": ["approve", "edit", "reject"],
|
|
27
|
-
"description": "
|
|
27
|
+
"description": "코드 실행 승인 필요",
|
|
28
|
+
"icon": "code",
|
|
29
|
+
},
|
|
30
|
+
# Ask user tool - requires user input (HITL)
|
|
31
|
+
# Uses 'edit' decision to pass user's response via modified args
|
|
32
|
+
"ask_user_tool": {
|
|
33
|
+
"allowed_decisions": ["approve", "edit", "reject"],
|
|
34
|
+
"description": "사용자 입력 대기 중",
|
|
35
|
+
"icon": "help",
|
|
28
36
|
},
|
|
29
37
|
# Safe operations - no approval needed
|
|
30
38
|
"markdown_tool": False,
|
|
31
39
|
"read_file_tool": {
|
|
32
40
|
"allowed_decisions": ["approve", "edit"],
|
|
33
|
-
"description": "
|
|
41
|
+
"description": "파일 읽기 실행 중",
|
|
42
|
+
"icon": "description",
|
|
34
43
|
},
|
|
35
44
|
"write_todos": False, # Todo updates don't need approval
|
|
36
45
|
# Search tools need HITL for client-side execution (auto-approved by frontend)
|
|
37
46
|
# Uses 'edit' decision to pass execution_result back
|
|
38
47
|
"search_notebook_cells_tool": {
|
|
39
48
|
"allowed_decisions": ["approve", "edit"],
|
|
40
|
-
"description": "
|
|
49
|
+
"description": "노트북 셀 검색 중",
|
|
50
|
+
"icon": "search",
|
|
41
51
|
},
|
|
42
52
|
# Resource check tool for client-side execution (auto-approved by frontend)
|
|
43
53
|
"check_resource_tool": {
|
|
44
54
|
"allowed_decisions": ["approve", "edit"],
|
|
45
|
-
"description": "
|
|
55
|
+
"description": "시스템 리소스 확인 중",
|
|
56
|
+
"icon": "analytics",
|
|
57
|
+
},
|
|
58
|
+
# Workspace exploration tools for client-side execution (auto-approved by frontend)
|
|
59
|
+
"list_workspace_tool": {
|
|
60
|
+
"allowed_decisions": ["approve", "edit"],
|
|
61
|
+
"description": "작업 공간 파일 목록 조회 중",
|
|
62
|
+
"icon": "folder",
|
|
63
|
+
},
|
|
64
|
+
"search_files_tool": {
|
|
65
|
+
"allowed_decisions": ["approve", "edit"],
|
|
66
|
+
"description": "파일 내용 검색 중",
|
|
67
|
+
"icon": "search",
|
|
46
68
|
},
|
|
47
69
|
"execute_command_tool": {
|
|
48
70
|
"allowed_decisions": ["approve", "edit", "reject"],
|
|
49
|
-
"description": "
|
|
71
|
+
"description": "쉘 명령어 실행 승인 필요",
|
|
72
|
+
"icon": "terminal",
|
|
50
73
|
},
|
|
51
74
|
# File write requires approval
|
|
52
75
|
"write_file_tool": {
|
|
53
76
|
"allowed_decisions": ["approve", "edit", "reject"],
|
|
54
|
-
"description": "
|
|
77
|
+
"description": "파일 쓰기 승인 필요",
|
|
78
|
+
"icon": "save",
|
|
55
79
|
},
|
|
56
80
|
# File edit requires approval (string replacement with diff preview)
|
|
57
81
|
"edit_file_tool": {
|
|
58
82
|
"allowed_decisions": ["approve", "edit", "reject"],
|
|
59
|
-
"description": "
|
|
83
|
+
"description": "파일 수정 승인 필요",
|
|
84
|
+
"icon": "edit",
|
|
85
|
+
},
|
|
86
|
+
# Multi-edit requires approval (multiple string replacements atomically)
|
|
87
|
+
"multiedit_file_tool": {
|
|
88
|
+
"allowed_decisions": ["approve", "edit", "reject"],
|
|
89
|
+
"description": "다중 파일 수정 승인 필요",
|
|
90
|
+
"icon": "edit",
|
|
60
91
|
},
|
|
61
|
-
# Final answer doesn't need approval
|
|
62
|
-
"final_answer_tool": False,
|
|
63
92
|
}
|
|
@@ -4,7 +4,6 @@ LLM Factory for LangChain agent.
|
|
|
4
4
|
Provides functions to create LangChain LLM instances from configuration.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import json
|
|
8
7
|
import logging
|
|
9
8
|
from typing import Any, Dict
|
|
10
9
|
|
|
@@ -13,74 +12,6 @@ from agent_server.langchain.logging_utils import LLMTraceLogger
|
|
|
13
12
|
logger = logging.getLogger(__name__)
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
def _stringify_content(content: Any) -> str:
|
|
17
|
-
if content is None:
|
|
18
|
-
return ""
|
|
19
|
-
if isinstance(content, list):
|
|
20
|
-
parts = []
|
|
21
|
-
for part in content:
|
|
22
|
-
if isinstance(part, str):
|
|
23
|
-
parts.append(part)
|
|
24
|
-
elif isinstance(part, dict):
|
|
25
|
-
if part.get("type") == "text":
|
|
26
|
-
parts.append(part.get("text", ""))
|
|
27
|
-
elif "text" in part:
|
|
28
|
-
parts.append(part.get("text", ""))
|
|
29
|
-
return "\n".join(p for p in parts if p)
|
|
30
|
-
return str(content)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _summarize_payload(
|
|
34
|
-
payload: Dict[str, Any],
|
|
35
|
-
max_preview: int = 200,
|
|
36
|
-
max_items: int = 12,
|
|
37
|
-
) -> Dict[str, Any]:
|
|
38
|
-
summary: Dict[str, Any] = {}
|
|
39
|
-
|
|
40
|
-
def summarize_messages(key: str) -> None:
|
|
41
|
-
items = payload.get(key)
|
|
42
|
-
if not isinstance(items, list):
|
|
43
|
-
return
|
|
44
|
-
summarized = []
|
|
45
|
-
for item in items[:max_items]:
|
|
46
|
-
if not isinstance(item, dict):
|
|
47
|
-
summarized.append({"type": type(item).__name__})
|
|
48
|
-
continue
|
|
49
|
-
content_text = _stringify_content(item.get("content", ""))
|
|
50
|
-
entry = {
|
|
51
|
-
"role": item.get("role"),
|
|
52
|
-
"content_len": len(content_text),
|
|
53
|
-
"content_preview": content_text[:max_preview],
|
|
54
|
-
}
|
|
55
|
-
tool_calls = item.get("tool_calls")
|
|
56
|
-
if isinstance(tool_calls, list):
|
|
57
|
-
entry["tool_calls"] = [
|
|
58
|
-
tc.get("function", {}).get("name") or tc.get("name")
|
|
59
|
-
for tc in tool_calls
|
|
60
|
-
if isinstance(tc, dict)
|
|
61
|
-
]
|
|
62
|
-
summarized.append(entry)
|
|
63
|
-
if len(items) > max_items:
|
|
64
|
-
summarized.append({"truncated": len(items) - max_items})
|
|
65
|
-
summary[key] = summarized
|
|
66
|
-
|
|
67
|
-
summarize_messages("messages")
|
|
68
|
-
summarize_messages("input")
|
|
69
|
-
|
|
70
|
-
tools = payload.get("tools")
|
|
71
|
-
if isinstance(tools, list):
|
|
72
|
-
summary["tools"] = [
|
|
73
|
-
tool.get("function", {}).get("name") or tool.get("name")
|
|
74
|
-
for tool in tools
|
|
75
|
-
if isinstance(tool, dict)
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
if "tool_choice" in payload:
|
|
79
|
-
summary["tool_choice"] = payload.get("tool_choice")
|
|
80
|
-
|
|
81
|
-
return summary
|
|
82
|
-
|
|
83
|
-
|
|
84
15
|
def create_llm(llm_config: Dict[str, Any]):
|
|
85
16
|
"""Create LangChain LLM from config.
|
|
86
17
|
|
|
@@ -168,22 +99,7 @@ def _create_vllm_llm(llm_config: Dict[str, Any], callbacks):
|
|
|
168
99
|
|
|
169
100
|
logger.info(f"Creating vLLM LLM with model: {model}, endpoint: {endpoint}")
|
|
170
101
|
|
|
171
|
-
|
|
172
|
-
if "gpt-oss" in model.lower():
|
|
173
|
-
|
|
174
|
-
class ChatOpenAIGptOss(ChatOpenAI):
|
|
175
|
-
def _get_request_payload(self, input_, *, stop=None, **kwargs):
|
|
176
|
-
payload = super()._get_request_payload(input_, stop=stop, **kwargs)
|
|
177
|
-
summary = _summarize_payload(payload)
|
|
178
|
-
logger.info(
|
|
179
|
-
"gpt-oss payload summary: %s",
|
|
180
|
-
json.dumps(summary, ensure_ascii=False),
|
|
181
|
-
)
|
|
182
|
-
return payload
|
|
183
|
-
|
|
184
|
-
llm_class = ChatOpenAIGptOss
|
|
185
|
-
|
|
186
|
-
return llm_class(
|
|
102
|
+
return ChatOpenAI(
|
|
187
103
|
model=model,
|
|
188
104
|
api_key=api_key,
|
|
189
105
|
base_url=f"{endpoint}/v1",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Middleware Module
|
|
3
|
+
|
|
4
|
+
Custom middleware for the multi-agent architecture:
|
|
5
|
+
- SubAgentMiddleware: Handles subagent delegation via task tool
|
|
6
|
+
- SkillMiddleware: Progressive skill loading for code generation agents
|
|
7
|
+
- Existing middleware from custom_middleware.py is also available
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from agent_server.langchain.middleware.skill_middleware import (
|
|
11
|
+
SkillMiddleware,
|
|
12
|
+
get_skill_middleware,
|
|
13
|
+
)
|
|
14
|
+
from agent_server.langchain.middleware.subagent_middleware import (
|
|
15
|
+
SubAgentMiddleware,
|
|
16
|
+
create_task_tool,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"SubAgentMiddleware",
|
|
21
|
+
"create_task_tool",
|
|
22
|
+
"SkillMiddleware",
|
|
23
|
+
"get_skill_middleware",
|
|
24
|
+
]
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeHistoryMiddleware
|
|
3
|
+
|
|
4
|
+
Middleware that tracks code execution history and automatically injects it
|
|
5
|
+
into subagent context when calling python_developer.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Tracks jupyter_cell_tool, write_file_tool, edit_file_tool, multiedit_file_tool
|
|
9
|
+
- Automatically injects code history into task_tool context for python_developer
|
|
10
|
+
- Summarizes older history if total context exceeds 50k tokens
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import threading
|
|
15
|
+
import tiktoken
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Token limit for context (including system prompt)
|
|
23
|
+
MAX_CONTEXT_TOKENS = 50000
|
|
24
|
+
# Number of recent history entries to keep in full detail
|
|
25
|
+
RECENT_HISTORY_COUNT = 3
|
|
26
|
+
# Approximate system prompt token count for python_developer
|
|
27
|
+
PYTHON_DEV_SYSTEM_PROMPT_TOKENS = 2000
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CodeHistoryEntry:
|
|
32
|
+
"""Represents a single code execution or file operation."""
|
|
33
|
+
|
|
34
|
+
tool_name: str # jupyter_cell_tool, write_file_tool, edit_file_tool, multiedit_file_tool
|
|
35
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
36
|
+
|
|
37
|
+
# For jupyter_cell_tool
|
|
38
|
+
code: Optional[str] = None
|
|
39
|
+
output: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
# For file operations
|
|
42
|
+
file_path: Optional[str] = None
|
|
43
|
+
content: Optional[str] = None # For write_file
|
|
44
|
+
old_content: Optional[str] = None # For edit_file
|
|
45
|
+
new_content: Optional[str] = None # For edit_file
|
|
46
|
+
edits: Optional[List[Dict[str, str]]] = None # For multiedit_file
|
|
47
|
+
|
|
48
|
+
def to_context_string(self) -> str:
|
|
49
|
+
"""Convert entry to a formatted string for context injection."""
|
|
50
|
+
timestamp_str = self.timestamp.strftime("%H:%M:%S")
|
|
51
|
+
|
|
52
|
+
if self.tool_name == "jupyter_cell_tool":
|
|
53
|
+
output_preview = self._truncate(self.output, 500) if self.output else "(no output)"
|
|
54
|
+
return f"""## Cell ({timestamp_str})
|
|
55
|
+
```python
|
|
56
|
+
{self.code}
|
|
57
|
+
```
|
|
58
|
+
Output:
|
|
59
|
+
```
|
|
60
|
+
{output_preview}
|
|
61
|
+
```"""
|
|
62
|
+
|
|
63
|
+
elif self.tool_name == "write_file_tool":
|
|
64
|
+
content_preview = self._truncate(self.content, 300) if self.content else ""
|
|
65
|
+
return f"""## File Write ({timestamp_str})
|
|
66
|
+
Path: {self.file_path}
|
|
67
|
+
```
|
|
68
|
+
{content_preview}
|
|
69
|
+
```"""
|
|
70
|
+
|
|
71
|
+
elif self.tool_name == "edit_file_tool":
|
|
72
|
+
return f"""## File Edit ({timestamp_str})
|
|
73
|
+
Path: {self.file_path}
|
|
74
|
+
Change: `{self._truncate(self.old_content, 50)}` → `{self._truncate(self.new_content, 50)}`"""
|
|
75
|
+
|
|
76
|
+
elif self.tool_name == "multiedit_file_tool":
|
|
77
|
+
edit_count = len(self.edits) if self.edits else 0
|
|
78
|
+
return f"""## File MultiEdit ({timestamp_str})
|
|
79
|
+
Path: {self.file_path}
|
|
80
|
+
Changes: {edit_count} edits applied"""
|
|
81
|
+
|
|
82
|
+
return f"## Unknown ({timestamp_str})"
|
|
83
|
+
|
|
84
|
+
def to_summary_string(self) -> str:
|
|
85
|
+
"""Convert entry to a brief summary string."""
|
|
86
|
+
if self.tool_name == "jupyter_cell_tool":
|
|
87
|
+
# Extract first meaningful line of code
|
|
88
|
+
if self.code:
|
|
89
|
+
first_line = self.code.strip().split('\n')[0][:60]
|
|
90
|
+
return f"- Cell: {first_line}..."
|
|
91
|
+
return "- Cell: (empty)"
|
|
92
|
+
|
|
93
|
+
elif self.tool_name == "write_file_tool":
|
|
94
|
+
return f"- Write: {self.file_path}"
|
|
95
|
+
|
|
96
|
+
elif self.tool_name == "edit_file_tool":
|
|
97
|
+
return f"- Edit: {self.file_path}"
|
|
98
|
+
|
|
99
|
+
elif self.tool_name == "multiedit_file_tool":
|
|
100
|
+
edit_count = len(self.edits) if self.edits else 0
|
|
101
|
+
return f"- MultiEdit: {self.file_path} ({edit_count} changes)"
|
|
102
|
+
|
|
103
|
+
return "- Unknown operation"
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _truncate(text: Optional[str], max_length: int) -> str:
|
|
107
|
+
"""Truncate text to max_length."""
|
|
108
|
+
if not text:
|
|
109
|
+
return ""
|
|
110
|
+
if len(text) <= max_length:
|
|
111
|
+
return text
|
|
112
|
+
return text[:max_length] + "..."
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class CodeHistoryTracker:
|
|
116
|
+
"""
|
|
117
|
+
Thread-safe tracker for code execution history.
|
|
118
|
+
|
|
119
|
+
Tracks all code-related tool executions and provides methods to:
|
|
120
|
+
- Add new entries
|
|
121
|
+
- Get formatted history for context injection
|
|
122
|
+
- Summarize old entries when context is too large
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self):
|
|
126
|
+
self._history: List[CodeHistoryEntry] = []
|
|
127
|
+
self._lock = threading.Lock()
|
|
128
|
+
self._tokenizer = None
|
|
129
|
+
|
|
130
|
+
def _get_tokenizer(self):
|
|
131
|
+
"""Lazy load tokenizer."""
|
|
132
|
+
if self._tokenizer is None:
|
|
133
|
+
try:
|
|
134
|
+
self._tokenizer = tiktoken.get_encoding("cl100k_base")
|
|
135
|
+
except Exception:
|
|
136
|
+
self._tokenizer = None
|
|
137
|
+
return self._tokenizer
|
|
138
|
+
|
|
139
|
+
def _count_tokens(self, text: str) -> int:
|
|
140
|
+
"""Count tokens in text."""
|
|
141
|
+
tokenizer = self._get_tokenizer()
|
|
142
|
+
if tokenizer:
|
|
143
|
+
try:
|
|
144
|
+
return len(tokenizer.encode(text))
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
# Fallback: rough estimate (4 chars per token for mixed content)
|
|
148
|
+
return len(text) // 4
|
|
149
|
+
|
|
150
|
+
def add_jupyter_cell(self, code: str, output: str) -> None:
|
|
151
|
+
"""Track a jupyter_cell_tool execution."""
|
|
152
|
+
with self._lock:
|
|
153
|
+
entry = CodeHistoryEntry(
|
|
154
|
+
tool_name="jupyter_cell_tool",
|
|
155
|
+
code=code,
|
|
156
|
+
output=output,
|
|
157
|
+
)
|
|
158
|
+
self._history.append(entry)
|
|
159
|
+
logger.info(f"CodeHistory: Added jupyter_cell (total: {len(self._history)})")
|
|
160
|
+
|
|
161
|
+
def add_write_file(self, file_path: str, content: str) -> None:
|
|
162
|
+
"""Track a write_file_tool execution."""
|
|
163
|
+
with self._lock:
|
|
164
|
+
entry = CodeHistoryEntry(
|
|
165
|
+
tool_name="write_file_tool",
|
|
166
|
+
file_path=file_path,
|
|
167
|
+
content=content,
|
|
168
|
+
)
|
|
169
|
+
self._history.append(entry)
|
|
170
|
+
logger.info(f"CodeHistory: Added write_file {file_path} (total: {len(self._history)})")
|
|
171
|
+
|
|
172
|
+
def add_edit_file(self, file_path: str, old_content: str, new_content: str) -> None:
|
|
173
|
+
"""Track an edit_file_tool execution."""
|
|
174
|
+
with self._lock:
|
|
175
|
+
entry = CodeHistoryEntry(
|
|
176
|
+
tool_name="edit_file_tool",
|
|
177
|
+
file_path=file_path,
|
|
178
|
+
old_content=old_content,
|
|
179
|
+
new_content=new_content,
|
|
180
|
+
)
|
|
181
|
+
self._history.append(entry)
|
|
182
|
+
logger.info(f"CodeHistory: Added edit_file {file_path} (total: {len(self._history)})")
|
|
183
|
+
|
|
184
|
+
def add_multiedit_file(self, file_path: str, edits: List[Dict[str, str]]) -> None:
|
|
185
|
+
"""Track a multiedit_file_tool execution."""
|
|
186
|
+
with self._lock:
|
|
187
|
+
entry = CodeHistoryEntry(
|
|
188
|
+
tool_name="multiedit_file_tool",
|
|
189
|
+
file_path=file_path,
|
|
190
|
+
edits=edits,
|
|
191
|
+
)
|
|
192
|
+
self._history.append(entry)
|
|
193
|
+
logger.info(f"CodeHistory: Added multiedit_file {file_path} (total: {len(self._history)})")
|
|
194
|
+
|
|
195
|
+
def get_context_for_subagent(
|
|
196
|
+
self,
|
|
197
|
+
existing_context: Optional[str] = None,
|
|
198
|
+
max_tokens: int = MAX_CONTEXT_TOKENS,
|
|
199
|
+
system_prompt_tokens: int = PYTHON_DEV_SYSTEM_PROMPT_TOKENS,
|
|
200
|
+
) -> str:
|
|
201
|
+
"""
|
|
202
|
+
Get formatted code history for subagent context injection.
|
|
203
|
+
|
|
204
|
+
If total context exceeds max_tokens, older entries are summarized.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
existing_context: Existing context from task_tool call
|
|
208
|
+
max_tokens: Maximum total tokens allowed
|
|
209
|
+
system_prompt_tokens: Estimated tokens for system prompt
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Formatted context string with code history
|
|
213
|
+
"""
|
|
214
|
+
with self._lock:
|
|
215
|
+
if not self._history:
|
|
216
|
+
return existing_context or ""
|
|
217
|
+
|
|
218
|
+
# Calculate available tokens for history
|
|
219
|
+
existing_tokens = self._count_tokens(existing_context) if existing_context else 0
|
|
220
|
+
available_tokens = max_tokens - system_prompt_tokens - existing_tokens - 500 # 500 buffer
|
|
221
|
+
|
|
222
|
+
# Build full history string
|
|
223
|
+
full_history = self._build_full_history()
|
|
224
|
+
full_history_tokens = self._count_tokens(full_history)
|
|
225
|
+
|
|
226
|
+
logger.info(
|
|
227
|
+
f"CodeHistory: existing={existing_tokens}, "
|
|
228
|
+
f"history={full_history_tokens}, "
|
|
229
|
+
f"available={available_tokens}, "
|
|
230
|
+
f"entries={len(self._history)}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Check if we need to summarize
|
|
234
|
+
if full_history_tokens <= available_tokens:
|
|
235
|
+
# No summarization needed
|
|
236
|
+
history_section = full_history
|
|
237
|
+
else:
|
|
238
|
+
# Need to summarize older entries
|
|
239
|
+
history_section = self._build_summarized_history(available_tokens)
|
|
240
|
+
|
|
241
|
+
# Combine with existing context
|
|
242
|
+
if existing_context:
|
|
243
|
+
return f"""{existing_context}
|
|
244
|
+
|
|
245
|
+
[코드 히스토리]
|
|
246
|
+
{history_section}"""
|
|
247
|
+
else:
|
|
248
|
+
return f"""[코드 히스토리]
|
|
249
|
+
{history_section}"""
|
|
250
|
+
|
|
251
|
+
def _build_full_history(self) -> str:
|
|
252
|
+
"""Build full history string without summarization."""
|
|
253
|
+
if not self._history:
|
|
254
|
+
return ""
|
|
255
|
+
|
|
256
|
+
parts = []
|
|
257
|
+
for i, entry in enumerate(self._history, 1):
|
|
258
|
+
parts.append(f"### {i}. {entry.to_context_string()}")
|
|
259
|
+
|
|
260
|
+
return "\n\n".join(parts)
|
|
261
|
+
|
|
262
|
+
def _build_summarized_history(self, available_tokens: int) -> str:
|
|
263
|
+
"""Build history with older entries summarized."""
|
|
264
|
+
if len(self._history) <= RECENT_HISTORY_COUNT:
|
|
265
|
+
# Not enough entries to summarize, just truncate
|
|
266
|
+
return self._build_full_history()
|
|
267
|
+
|
|
268
|
+
# Split into old (to summarize) and recent (keep full)
|
|
269
|
+
old_entries = self._history[:-RECENT_HISTORY_COUNT]
|
|
270
|
+
recent_entries = self._history[-RECENT_HISTORY_COUNT:]
|
|
271
|
+
|
|
272
|
+
# Build summary of old entries
|
|
273
|
+
summary_parts = ["[이전 작업 요약]"]
|
|
274
|
+
for entry in old_entries:
|
|
275
|
+
summary_parts.append(entry.to_summary_string())
|
|
276
|
+
summary_section = "\n".join(summary_parts)
|
|
277
|
+
|
|
278
|
+
# Build full recent entries
|
|
279
|
+
recent_parts = ["\n[최근 코드 상세]"]
|
|
280
|
+
for i, entry in enumerate(recent_entries, len(old_entries) + 1):
|
|
281
|
+
recent_parts.append(f"### {i}. {entry.to_context_string()}")
|
|
282
|
+
recent_section = "\n\n".join(recent_parts)
|
|
283
|
+
|
|
284
|
+
combined = f"{summary_section}\n{recent_section}"
|
|
285
|
+
|
|
286
|
+
# Check if still too long
|
|
287
|
+
combined_tokens = self._count_tokens(combined)
|
|
288
|
+
if combined_tokens > available_tokens:
|
|
289
|
+
logger.warning(
|
|
290
|
+
f"CodeHistory: Even summarized history ({combined_tokens}) "
|
|
291
|
+
f"exceeds available tokens ({available_tokens}). Truncating."
|
|
292
|
+
)
|
|
293
|
+
# Further truncate by reducing recent entries
|
|
294
|
+
recent_parts = ["\n[최근 코드 (일부)]"]
|
|
295
|
+
for entry in recent_entries[-2:]: # Keep only last 2
|
|
296
|
+
recent_parts.append(entry.to_context_string())
|
|
297
|
+
recent_section = "\n\n".join(recent_parts)
|
|
298
|
+
combined = f"{summary_section}\n{recent_section}"
|
|
299
|
+
|
|
300
|
+
return combined
|
|
301
|
+
|
|
302
|
+
def clear(self) -> None:
|
|
303
|
+
"""Clear all history."""
|
|
304
|
+
with self._lock:
|
|
305
|
+
self._history.clear()
|
|
306
|
+
logger.info("CodeHistory: Cleared all history")
|
|
307
|
+
|
|
308
|
+
def get_entry_count(self) -> int:
|
|
309
|
+
"""Get number of history entries."""
|
|
310
|
+
with self._lock:
|
|
311
|
+
return len(self._history)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# Global tracker instance (per-thread tracking could be added if needed)
|
|
315
|
+
_code_history_tracker: Optional[CodeHistoryTracker] = None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def get_code_history_tracker() -> CodeHistoryTracker:
|
|
319
|
+
"""Get the global CodeHistoryTracker instance."""
|
|
320
|
+
global _code_history_tracker
|
|
321
|
+
if _code_history_tracker is None:
|
|
322
|
+
_code_history_tracker = CodeHistoryTracker()
|
|
323
|
+
return _code_history_tracker
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def track_jupyter_cell(code: str, output: str) -> None:
|
|
327
|
+
"""Convenience function to track jupyter_cell_tool execution."""
|
|
328
|
+
get_code_history_tracker().add_jupyter_cell(code, output)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def track_write_file(file_path: str, content: str) -> None:
|
|
332
|
+
"""Convenience function to track write_file_tool execution."""
|
|
333
|
+
get_code_history_tracker().add_write_file(file_path, content)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def track_edit_file(file_path: str, old_content: str, new_content: str) -> None:
|
|
337
|
+
"""Convenience function to track edit_file_tool execution."""
|
|
338
|
+
get_code_history_tracker().add_edit_file(file_path, old_content, new_content)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def track_multiedit_file(file_path: str, edits: List[Dict[str, str]]) -> None:
|
|
342
|
+
"""Convenience function to track multiedit_file_tool execution."""
|
|
343
|
+
get_code_history_tracker().add_multiedit_file(file_path, edits)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def get_context_with_history(existing_context: Optional[str] = None) -> str:
|
|
347
|
+
"""Get context string with code history injected."""
|
|
348
|
+
return get_code_history_tracker().get_context_for_subagent(existing_context)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def clear_code_history() -> None:
|
|
352
|
+
"""Clear all code history."""
|
|
353
|
+
get_code_history_tracker().clear()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def track_tool_execution(tool_name: str, args: Dict[str, Any]) -> None:
|
|
357
|
+
"""
|
|
358
|
+
Track a tool execution from HITL decision processing.
|
|
359
|
+
|
|
360
|
+
This function is called from langchain_agent.py when an EDIT decision
|
|
361
|
+
is processed with execution_result.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
tool_name: Name of the tool (jupyter_cell_tool, write_file_tool, etc.)
|
|
365
|
+
args: Tool arguments including execution_result
|
|
366
|
+
"""
|
|
367
|
+
if not args:
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
execution_result = args.get("execution_result", {})
|
|
371
|
+
if not execution_result:
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
tracker = get_code_history_tracker()
|
|
375
|
+
|
|
376
|
+
if tool_name == "jupyter_cell_tool":
|
|
377
|
+
code = args.get("code", "")
|
|
378
|
+
output = execution_result.get("output", "")
|
|
379
|
+
if code:
|
|
380
|
+
tracker.add_jupyter_cell(code, output)
|
|
381
|
+
logger.info(f"CodeHistory: Tracked jupyter_cell execution (code len={len(code)})")
|
|
382
|
+
|
|
383
|
+
elif tool_name == "write_file_tool":
|
|
384
|
+
file_path = args.get("path", "")
|
|
385
|
+
content = args.get("content", "")
|
|
386
|
+
if file_path:
|
|
387
|
+
tracker.add_write_file(file_path, content)
|
|
388
|
+
logger.info(f"CodeHistory: Tracked write_file to {file_path}")
|
|
389
|
+
|
|
390
|
+
elif tool_name == "edit_file_tool":
|
|
391
|
+
file_path = args.get("path", "")
|
|
392
|
+
old_string = args.get("old_string", "")
|
|
393
|
+
new_string = args.get("new_string", "")
|
|
394
|
+
if file_path:
|
|
395
|
+
tracker.add_edit_file(file_path, old_string, new_string)
|
|
396
|
+
logger.info(f"CodeHistory: Tracked edit_file to {file_path}")
|
|
397
|
+
|
|
398
|
+
elif tool_name == "multiedit_file_tool":
|
|
399
|
+
file_path = args.get("path", "")
|
|
400
|
+
edits = args.get("edits", [])
|
|
401
|
+
if file_path:
|
|
402
|
+
# Convert edits to list of dicts if needed
|
|
403
|
+
edits_as_dicts = []
|
|
404
|
+
for edit in edits:
|
|
405
|
+
if hasattr(edit, "model_dump"):
|
|
406
|
+
edits_as_dicts.append(edit.model_dump())
|
|
407
|
+
elif hasattr(edit, "dict"):
|
|
408
|
+
edits_as_dicts.append(edit.dict())
|
|
409
|
+
elif isinstance(edit, dict):
|
|
410
|
+
edits_as_dicts.append(edit)
|
|
411
|
+
tracker.add_multiedit_file(file_path, edits_as_dicts)
|
|
412
|
+
logger.info(f"CodeHistory: Tracked multiedit_file to {file_path} ({len(edits)} edits)")
|