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.
Files changed (106) hide show
  1. agent_server/core/notebook_generator.py +4 -4
  2. agent_server/langchain/MULTI_AGENT_ARCHITECTURE.md +1114 -0
  3. agent_server/langchain/__init__.py +2 -2
  4. agent_server/langchain/agent.py +72 -33
  5. agent_server/langchain/agent_factory.py +400 -0
  6. agent_server/langchain/agent_prompts/__init__.py +25 -0
  7. agent_server/langchain/agent_prompts/athena_query_prompt.py +71 -0
  8. agent_server/langchain/agent_prompts/planner_prompt.py +85 -0
  9. agent_server/langchain/agent_prompts/python_developer_prompt.py +123 -0
  10. agent_server/langchain/agent_prompts/researcher_prompt.py +38 -0
  11. agent_server/langchain/custom_middleware.py +656 -113
  12. agent_server/langchain/hitl_config.py +38 -9
  13. agent_server/langchain/llm_factory.py +1 -85
  14. agent_server/langchain/middleware/__init__.py +24 -0
  15. agent_server/langchain/middleware/code_history_middleware.py +412 -0
  16. agent_server/langchain/middleware/description_injector.py +150 -0
  17. agent_server/langchain/middleware/skill_middleware.py +298 -0
  18. agent_server/langchain/middleware/subagent_events.py +171 -0
  19. agent_server/langchain/middleware/subagent_middleware.py +329 -0
  20. agent_server/langchain/prompts.py +107 -135
  21. agent_server/langchain/skills/data_analysis.md +236 -0
  22. agent_server/langchain/skills/data_loading.md +158 -0
  23. agent_server/langchain/skills/inference.md +392 -0
  24. agent_server/langchain/skills/model_training.md +318 -0
  25. agent_server/langchain/skills/pyspark.md +352 -0
  26. agent_server/langchain/subagents/__init__.py +20 -0
  27. agent_server/langchain/subagents/base.py +173 -0
  28. agent_server/langchain/tools/__init__.py +3 -0
  29. agent_server/langchain/tools/jupyter_tools.py +58 -20
  30. agent_server/langchain/tools/lsp_tools.py +1 -1
  31. agent_server/langchain/tools/shared/__init__.py +26 -0
  32. agent_server/langchain/tools/shared/qdrant_search.py +175 -0
  33. agent_server/langchain/tools/tool_registry.py +219 -0
  34. agent_server/langchain/tools/workspace_tools.py +197 -0
  35. agent_server/prompts/file_action_prompts.py +8 -8
  36. agent_server/routers/config.py +40 -1
  37. agent_server/routers/langchain_agent.py +868 -321
  38. hdsp_agent_core/__init__.py +46 -47
  39. hdsp_agent_core/factory.py +6 -10
  40. hdsp_agent_core/interfaces.py +4 -2
  41. hdsp_agent_core/knowledge/__init__.py +5 -5
  42. hdsp_agent_core/knowledge/chunking.py +87 -61
  43. hdsp_agent_core/knowledge/loader.py +103 -101
  44. hdsp_agent_core/llm/service.py +192 -107
  45. hdsp_agent_core/managers/config_manager.py +16 -22
  46. hdsp_agent_core/managers/session_manager.py +5 -4
  47. hdsp_agent_core/models/__init__.py +12 -12
  48. hdsp_agent_core/models/agent.py +15 -8
  49. hdsp_agent_core/models/common.py +1 -2
  50. hdsp_agent_core/models/rag.py +48 -111
  51. hdsp_agent_core/prompts/__init__.py +12 -12
  52. hdsp_agent_core/prompts/cell_action_prompts.py +9 -7
  53. hdsp_agent_core/services/agent_service.py +10 -8
  54. hdsp_agent_core/services/chat_service.py +10 -6
  55. hdsp_agent_core/services/rag_service.py +3 -6
  56. hdsp_agent_core/tests/conftest.py +4 -1
  57. hdsp_agent_core/tests/test_factory.py +2 -2
  58. hdsp_agent_core/tests/test_services.py +12 -19
  59. {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
  60. {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.13.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +7 -2
  61. 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
  62. hdsp_jupyter_extension-2.0.13.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.037b3c8e5d6a92b63b16.js.map +1 -0
  63. 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
  64. hdsp_jupyter_extension-2.0.13.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.5449ba3c7e25177d2987.js.map +1 -0
  65. 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
  66. hdsp_jupyter_extension-2.0.13.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.a8e0b064eb9b1c1ff463.js.map +1 -0
  67. {hdsp_jupyter_extension-2.0.10.dist-info → hdsp_jupyter_extension-2.0.13.dist-info}/METADATA +1 -1
  68. {hdsp_jupyter_extension-2.0.10.dist-info → hdsp_jupyter_extension-2.0.13.dist-info}/RECORD +100 -76
  69. jupyter_ext/__init__.py +21 -11
  70. jupyter_ext/_version.py +1 -1
  71. jupyter_ext/handlers.py +128 -58
  72. jupyter_ext/labextension/build_log.json +1 -1
  73. jupyter_ext/labextension/package.json +7 -2
  74. jupyter_ext/labextension/static/{frontend_styles_index_js.2d9fb488c82498c45c2d.js → frontend_styles_index_js.037b3c8e5d6a92b63b16.js} +1108 -179
  75. jupyter_ext/labextension/static/frontend_styles_index_js.037b3c8e5d6a92b63b16.js.map +1 -0
  76. 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
  77. jupyter_ext/labextension/static/lib_index_js.5449ba3c7e25177d2987.js.map +1 -0
  78. jupyter_ext/labextension/static/{remoteEntry.4a252df3ade74efee8d6.js → remoteEntry.a8e0b064eb9b1c1ff463.js} +17 -17
  79. jupyter_ext/labextension/static/remoteEntry.a8e0b064eb9b1c1ff463.js.map +1 -0
  80. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +0 -1
  81. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js.map +0 -1
  82. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4a252df3ade74efee8d6.js.map +0 -1
  83. jupyter_ext/labextension/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +0 -1
  84. jupyter_ext/labextension/static/lib_index_js.dc6434bee96ab03a0539.js.map +0 -1
  85. jupyter_ext/labextension/static/remoteEntry.4a252df3ade74efee8d6.js.map +0 -1
  86. {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
  87. {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.13.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  88. {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
  89. {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
  90. {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
  91. {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
  92. {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
  93. {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
  94. {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
  95. {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
  96. {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
  97. {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
  98. {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
  99. {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
  100. {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
  101. {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
  102. {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
  103. {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
  104. {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
  105. {hdsp_jupyter_extension-2.0.10.dist-info → hdsp_jupyter_extension-2.0.13.dist-info}/WHEEL +0 -0
  106. {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": "🔍 Code execution requires approval",
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": "🔍 Searching notebook cells",
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": "📊 Checking system resources",
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": "🖥️ Shell command requires approval",
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": "File write requires approval",
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": "File edit requires approval",
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
- llm_class = ChatOpenAI
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)")