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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Description Injector Middleware
|
|
3
|
+
|
|
4
|
+
Automatically extracts description from python_developer's JSON response
|
|
5
|
+
and injects it into jupyter_cell_tool calls when description is missing.
|
|
6
|
+
|
|
7
|
+
This middleware only activates when:
|
|
8
|
+
1. python_developer returns a response (to extract description)
|
|
9
|
+
2. jupyter_cell_tool is called without description (to inject)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
from typing import Any, Dict, Optional
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Thread-local storage for pending description
|
|
19
|
+
_pending_description: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_pending_description() -> Optional[str]:
|
|
23
|
+
"""Get the pending description from last python_developer call."""
|
|
24
|
+
global _pending_description
|
|
25
|
+
logger.info(f"[DescriptionInjector] GET pending description: {_pending_description[:50] if _pending_description else 'None'}...")
|
|
26
|
+
return _pending_description
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_pending_description(description: Optional[str]) -> None:
|
|
30
|
+
"""Set the pending description."""
|
|
31
|
+
global _pending_description
|
|
32
|
+
_pending_description = description
|
|
33
|
+
if description:
|
|
34
|
+
logger.info(f"[DescriptionInjector] SET pending description: {description[:80]}...")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def clear_pending_description() -> None:
|
|
38
|
+
"""Clear the pending description after use."""
|
|
39
|
+
global _pending_description
|
|
40
|
+
_pending_description = None
|
|
41
|
+
logger.debug("[DescriptionInjector] Cleared pending description")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def extract_description_from_python_developer(response: str) -> Optional[str]:
|
|
45
|
+
"""
|
|
46
|
+
Extract description from python_developer's response.
|
|
47
|
+
|
|
48
|
+
Expected format:
|
|
49
|
+
[DESCRIPTION]
|
|
50
|
+
설명 내용 (2~3줄)
|
|
51
|
+
|
|
52
|
+
[CODE]
|
|
53
|
+
```python
|
|
54
|
+
코드
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Handles various edge cases and formatting variations.
|
|
58
|
+
"""
|
|
59
|
+
if not response:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
# Pattern 1: [DESCRIPTION] ... [CODE] (primary format)
|
|
63
|
+
# Handles newlines and various whitespace
|
|
64
|
+
patterns = [
|
|
65
|
+
# Standard format: [DESCRIPTION]\n...\n[CODE]
|
|
66
|
+
r'\[DESCRIPTION\]\s*\n(.*?)(?=\n\s*\[CODE\])',
|
|
67
|
+
# With extra whitespace: [DESCRIPTION] \n...\n [CODE]
|
|
68
|
+
r'\[DESCRIPTION\]\s*(.*?)(?=\s*\[CODE\])',
|
|
69
|
+
# Until code block: [DESCRIPTION]\n...\n```
|
|
70
|
+
r'\[DESCRIPTION\]\s*\n(.*?)(?=\n\s*```)',
|
|
71
|
+
# Until end of text if no [CODE] marker
|
|
72
|
+
r'\[DESCRIPTION\]\s*\n(.+?)(?=\n\n|\Z)',
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
for pattern in patterns:
|
|
76
|
+
match = re.search(pattern, response, re.DOTALL | re.IGNORECASE)
|
|
77
|
+
if match:
|
|
78
|
+
description = match.group(1).strip()
|
|
79
|
+
# Clean up: remove leading/trailing empty lines
|
|
80
|
+
description = '\n'.join(line for line in description.split('\n') if line.strip() or description.count('\n') < 5)
|
|
81
|
+
description = description.strip()
|
|
82
|
+
if description and len(description) > 5: # Minimum meaningful description
|
|
83
|
+
logger.info(f"[DescriptionInjector] Extracted description: {description[:80]}...")
|
|
84
|
+
return description
|
|
85
|
+
|
|
86
|
+
# Fallback: Try to find description-like content at the start
|
|
87
|
+
# (for cases where format markers are missing)
|
|
88
|
+
lines = response.strip().split('\n')
|
|
89
|
+
if lines:
|
|
90
|
+
# Check if first few lines look like a description (no code markers)
|
|
91
|
+
potential_desc = []
|
|
92
|
+
for line in lines[:5]:
|
|
93
|
+
line = line.strip()
|
|
94
|
+
if line.startswith('```') or line.startswith('import ') or line.startswith('def ') or line.startswith('class '):
|
|
95
|
+
break
|
|
96
|
+
if line and not line.startswith('['):
|
|
97
|
+
potential_desc.append(line)
|
|
98
|
+
if potential_desc:
|
|
99
|
+
description = '\n'.join(potential_desc)
|
|
100
|
+
if 10 < len(description) < 500: # Reasonable description length
|
|
101
|
+
logger.info(f"[DescriptionInjector] Extracted description (fallback): {description[:80]}...")
|
|
102
|
+
return description
|
|
103
|
+
|
|
104
|
+
logger.debug("[DescriptionInjector] No description found in response")
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def process_task_tool_response(agent_name: str, response: str) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Process task_tool response to extract description if from python_developer.
|
|
111
|
+
|
|
112
|
+
Only activates when agent_name is 'python_developer'.
|
|
113
|
+
"""
|
|
114
|
+
if agent_name != "python_developer":
|
|
115
|
+
return response
|
|
116
|
+
|
|
117
|
+
description = extract_description_from_python_developer(response)
|
|
118
|
+
if description:
|
|
119
|
+
set_pending_description(description)
|
|
120
|
+
|
|
121
|
+
return response
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def inject_description_if_needed(tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
125
|
+
"""
|
|
126
|
+
Inject pending description into jupyter_cell_tool if description is missing.
|
|
127
|
+
|
|
128
|
+
Only activates when:
|
|
129
|
+
- tool_name is 'jupyter_cell_tool'
|
|
130
|
+
- description is None or empty
|
|
131
|
+
- there's a pending description available
|
|
132
|
+
"""
|
|
133
|
+
if tool_name != "jupyter_cell_tool":
|
|
134
|
+
return args
|
|
135
|
+
|
|
136
|
+
# Check if description is already provided
|
|
137
|
+
current_desc = args.get("description")
|
|
138
|
+
if current_desc:
|
|
139
|
+
# Description already provided, no need to inject
|
|
140
|
+
return args
|
|
141
|
+
|
|
142
|
+
# Try to inject pending description
|
|
143
|
+
pending = get_pending_description()
|
|
144
|
+
if pending:
|
|
145
|
+
logger.info(f"[DescriptionInjector] Injecting description into jupyter_cell_tool: {pending[:50]}...")
|
|
146
|
+
args = dict(args) # Make a copy
|
|
147
|
+
args["description"] = pending
|
|
148
|
+
clear_pending_description() # Use once
|
|
149
|
+
|
|
150
|
+
return args
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SkillMiddleware
|
|
3
|
+
|
|
4
|
+
Middleware that provides progressive skill loading for code generation agents.
|
|
5
|
+
Based on LangChain Skills pattern for token-efficient context injection.
|
|
6
|
+
|
|
7
|
+
Key features:
|
|
8
|
+
- Injects skill metadata into system prompt (~250 tokens)
|
|
9
|
+
- Provides load_skill tool for on-demand full content loading
|
|
10
|
+
- Reads skill definitions from markdown files
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
from langchain_core.tools import tool
|
|
20
|
+
from pydantic import BaseModel, Field
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Skills directory path
|
|
25
|
+
SKILLS_DIR = Path(__file__).parent.parent / "skills"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_skill_metadata(file_path: Path) -> Optional[Dict[str, str]]:
|
|
29
|
+
"""
|
|
30
|
+
Parse skill metadata from markdown file's YAML frontmatter.
|
|
31
|
+
|
|
32
|
+
Expected format:
|
|
33
|
+
---
|
|
34
|
+
name: skill-name
|
|
35
|
+
description: Skill description
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dict with 'name', 'description', 'path' or None if invalid
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
content = file_path.read_text(encoding="utf-8")
|
|
43
|
+
|
|
44
|
+
# Check for YAML frontmatter
|
|
45
|
+
if not content.startswith("---"):
|
|
46
|
+
logger.warning(f"Skill file missing YAML frontmatter: {file_path}")
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
# Extract frontmatter
|
|
50
|
+
parts = content.split("---", 2)
|
|
51
|
+
if len(parts) < 3:
|
|
52
|
+
logger.warning(f"Invalid YAML frontmatter format: {file_path}")
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
frontmatter = yaml.safe_load(parts[1])
|
|
56
|
+
if not frontmatter or "name" not in frontmatter:
|
|
57
|
+
logger.warning(f"Missing 'name' in frontmatter: {file_path}")
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
"name": frontmatter.get("name"),
|
|
62
|
+
"description": frontmatter.get("description", "No description"),
|
|
63
|
+
"path": str(file_path),
|
|
64
|
+
}
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"Failed to parse skill file {file_path}: {e}")
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _load_all_skills() -> Dict[str, Dict[str, str]]:
|
|
71
|
+
"""
|
|
72
|
+
Load all skill metadata from the skills directory.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Dict mapping skill name to metadata (name, description, path)
|
|
76
|
+
"""
|
|
77
|
+
skills = {}
|
|
78
|
+
|
|
79
|
+
if not SKILLS_DIR.exists():
|
|
80
|
+
logger.warning(f"Skills directory not found: {SKILLS_DIR}")
|
|
81
|
+
return skills
|
|
82
|
+
|
|
83
|
+
for file_path in SKILLS_DIR.glob("*.md"):
|
|
84
|
+
metadata = _parse_skill_metadata(file_path)
|
|
85
|
+
if metadata:
|
|
86
|
+
skills[metadata["name"]] = metadata
|
|
87
|
+
logger.debug(f"Loaded skill: {metadata['name']}")
|
|
88
|
+
|
|
89
|
+
logger.info(f"Loaded {len(skills)} skills from {SKILLS_DIR}")
|
|
90
|
+
return skills
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_skill_content(skill_name: str, skills: Dict[str, Dict[str, str]]) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Load full content of a skill file (excluding YAML frontmatter).
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
skill_name: Name of the skill to load
|
|
99
|
+
skills: Dictionary of skill metadata
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Full skill content as string, or error message
|
|
103
|
+
"""
|
|
104
|
+
if skill_name not in skills:
|
|
105
|
+
available = ", ".join(skills.keys())
|
|
106
|
+
return f"Error: Unknown skill '{skill_name}'. Available skills: {available}"
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
file_path = Path(skills[skill_name]["path"])
|
|
110
|
+
content = file_path.read_text(encoding="utf-8")
|
|
111
|
+
|
|
112
|
+
# Remove YAML frontmatter
|
|
113
|
+
if content.startswith("---"):
|
|
114
|
+
parts = content.split("---", 2)
|
|
115
|
+
if len(parts) >= 3:
|
|
116
|
+
content = parts[2].strip()
|
|
117
|
+
|
|
118
|
+
return content
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return f"Error loading skill '{skill_name}': {str(e)}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _build_skills_prompt_section(skills: Dict[str, Dict[str, str]]) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Build the Available Skills section for system prompt injection.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
skills: Dictionary of skill metadata
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Formatted string for system prompt (~250 tokens)
|
|
132
|
+
"""
|
|
133
|
+
if not skills:
|
|
134
|
+
return ""
|
|
135
|
+
|
|
136
|
+
lines = ["## Available Skills"]
|
|
137
|
+
lines.append("")
|
|
138
|
+
lines.append("### Context 리소스 정보")
|
|
139
|
+
lines.append("Main Agent가 task 호출 시 제공하는 Context에는 다음 정보가 포함됩니다:")
|
|
140
|
+
lines.append("- **파일 크기**: `file_size: 200MB`, `total_size: 1.5GB` 등")
|
|
141
|
+
lines.append("- **시스템 메모리**: `available_memory: 16GB`, `gpu_memory: 8GB` 등")
|
|
142
|
+
lines.append("- **데이터 행 수**: `row_count: 10000000` 등")
|
|
143
|
+
lines.append("")
|
|
144
|
+
lines.append("### 스킬 로드 규칙 (MANDATORY)")
|
|
145
|
+
lines.append("**아래 조건에 해당하면 반드시 load_skill_tool()을 먼저 호출하세요:**")
|
|
146
|
+
lines.append("1. 파일 크기 >= 100MB → `load_skill_tool('data_loading')`")
|
|
147
|
+
lines.append("2. DataFrame 행 수 >= 100만 → `load_skill_tool('data_analysis')`")
|
|
148
|
+
lines.append("3. GPU/CUDA 사용 또는 모델 훈련 → `load_skill_tool('model_training')`")
|
|
149
|
+
lines.append("4. 모델 추론 최적화 필요 → `load_skill_tool('inference')`")
|
|
150
|
+
lines.append("5. PySpark/분산처리 → `load_skill_tool('pyspark')`")
|
|
151
|
+
lines.append("")
|
|
152
|
+
|
|
153
|
+
for name, metadata in sorted(skills.items()):
|
|
154
|
+
lines.append(f"- **{name}**: {metadata['description']}")
|
|
155
|
+
|
|
156
|
+
lines.append("")
|
|
157
|
+
lines.append("### 스킬 미사용 시점")
|
|
158
|
+
lines.append("- 단순 print(), 기본 연산")
|
|
159
|
+
lines.append("- 소형 파일 (< 100MB) 기본 처리")
|
|
160
|
+
lines.append("- DataFrame 행 수 < 100만")
|
|
161
|
+
lines.append("- Context에 리소스 정보가 명시되지 않은 경우")
|
|
162
|
+
|
|
163
|
+
return "\n".join(lines)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def create_load_skill_tool(skills: Dict[str, Dict[str, str]]):
|
|
167
|
+
"""
|
|
168
|
+
Create the load_skill_tool for on-demand skill loading.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
skills: Dictionary of skill metadata
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
LangChain tool for loading skills
|
|
175
|
+
"""
|
|
176
|
+
available_skills = ", ".join(sorted(skills.keys()))
|
|
177
|
+
|
|
178
|
+
class LoadSkillInput(BaseModel):
|
|
179
|
+
"""Input schema for load_skill_tool"""
|
|
180
|
+
skill_name: str = Field(
|
|
181
|
+
description=f"Name of the skill to load. Available: {available_skills}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@tool(args_schema=LoadSkillInput)
|
|
185
|
+
def load_skill_tool(skill_name: str) -> str:
|
|
186
|
+
"""
|
|
187
|
+
Load detailed optimization guide for a specific skill.
|
|
188
|
+
|
|
189
|
+
Use this tool when you need specific optimization patterns for:
|
|
190
|
+
- data_loading: Large file handling (chunking, dtype, Dask)
|
|
191
|
+
- data_analysis: DataFrame operations (vectorization, groupby)
|
|
192
|
+
- model_training: GPU/memory optimization (fp16, gradient checkpointing)
|
|
193
|
+
- inference: Model serving optimization (batching, quantization, TensorRT)
|
|
194
|
+
- pyspark: Distributed processing (partitioning, caching, broadcast join)
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
skill_name: Name of the skill to load
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Full optimization guide with code patterns and best practices
|
|
201
|
+
"""
|
|
202
|
+
logger.info(f"Loading skill: {skill_name}")
|
|
203
|
+
|
|
204
|
+
# Emit subagent tool call event for UI
|
|
205
|
+
try:
|
|
206
|
+
from agent_server.langchain.middleware.subagent_events import emit_subagent_tool_call
|
|
207
|
+
# Pass explicit subagent name as fallback since thread-local context may not be available
|
|
208
|
+
emit_subagent_tool_call(
|
|
209
|
+
"load_skill_tool",
|
|
210
|
+
{"skill_name": skill_name},
|
|
211
|
+
subagent_name="python_developer" # load_skill is only used by python_developer
|
|
212
|
+
)
|
|
213
|
+
logger.info(f"Emitted load_skill_tool event for skill: {skill_name}")
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.warning(f"Failed to emit load_skill_tool event: {e}")
|
|
216
|
+
content = _get_skill_content(skill_name, skills)
|
|
217
|
+
|
|
218
|
+
# Log content length for monitoring
|
|
219
|
+
if not content.startswith("Error"):
|
|
220
|
+
logger.info(f"Skill '{skill_name}' loaded: {len(content)} chars")
|
|
221
|
+
|
|
222
|
+
return content
|
|
223
|
+
|
|
224
|
+
return load_skill_tool
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class SkillMiddleware:
|
|
228
|
+
"""
|
|
229
|
+
Middleware that adds skill loading capability to code generation agents.
|
|
230
|
+
|
|
231
|
+
This middleware:
|
|
232
|
+
1. Injects Available Skills section into system prompt (~250 tokens)
|
|
233
|
+
2. Adds the `load_skill` tool for on-demand content loading
|
|
234
|
+
3. Enables token-efficient progressive disclosure of optimization guides
|
|
235
|
+
|
|
236
|
+
Usage:
|
|
237
|
+
skill_middleware = SkillMiddleware()
|
|
238
|
+
|
|
239
|
+
# Get prompt section to append to system prompt
|
|
240
|
+
skills_section = skill_middleware.get_prompt_section()
|
|
241
|
+
|
|
242
|
+
# Get load_skill tool to add to agent's tools
|
|
243
|
+
tools = skill_middleware.get_tools()
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def __init__(self):
|
|
247
|
+
"""Initialize SkillMiddleware by loading all skill metadata."""
|
|
248
|
+
self.skills = _load_all_skills()
|
|
249
|
+
self.prompt_section = _build_skills_prompt_section(self.skills)
|
|
250
|
+
self.load_skill_tool = create_load_skill_tool(self.skills)
|
|
251
|
+
|
|
252
|
+
logger.info(
|
|
253
|
+
f"SkillMiddleware initialized with {len(self.skills)} skills: "
|
|
254
|
+
f"{list(self.skills.keys())}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def get_prompt_section(self) -> str:
|
|
258
|
+
"""
|
|
259
|
+
Get the Available Skills section for system prompt.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Formatted string to append to system prompt
|
|
263
|
+
"""
|
|
264
|
+
return self.prompt_section
|
|
265
|
+
|
|
266
|
+
def get_tools(self) -> List[Any]:
|
|
267
|
+
"""
|
|
268
|
+
Get the tools provided by this middleware.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List containing the load_skill tool
|
|
272
|
+
"""
|
|
273
|
+
return [self.load_skill_tool]
|
|
274
|
+
|
|
275
|
+
def __call__(self, tools: List[Any]) -> List[Any]:
|
|
276
|
+
"""
|
|
277
|
+
Add load_skill tool to the agent's toolset.
|
|
278
|
+
|
|
279
|
+
This is called during agent creation to augment the tool list.
|
|
280
|
+
"""
|
|
281
|
+
return tools + [self.load_skill_tool]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# Singleton instance for reuse
|
|
285
|
+
_skill_middleware_instance: Optional[SkillMiddleware] = None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def get_skill_middleware() -> SkillMiddleware:
|
|
289
|
+
"""
|
|
290
|
+
Get the singleton SkillMiddleware instance.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
SkillMiddleware instance
|
|
294
|
+
"""
|
|
295
|
+
global _skill_middleware_instance
|
|
296
|
+
if _skill_middleware_instance is None:
|
|
297
|
+
_skill_middleware_instance = SkillMiddleware()
|
|
298
|
+
return _skill_middleware_instance
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subagent Event Queue
|
|
3
|
+
|
|
4
|
+
Provides a thread-safe queue for subagent events that can be
|
|
5
|
+
consumed by the main streaming loop for UI display.
|
|
6
|
+
|
|
7
|
+
Events include:
|
|
8
|
+
- subagent_start: When a subagent is invoked
|
|
9
|
+
- subagent_tool_call: When a subagent calls a tool
|
|
10
|
+
- subagent_complete: When a subagent finishes
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import threading
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from queue import Empty, Queue
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Thread-local storage for current subagent context
|
|
23
|
+
_subagent_context = threading.local()
|
|
24
|
+
|
|
25
|
+
# Global event queue (thread-safe)
|
|
26
|
+
_event_queue: Queue = Queue()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class SubagentEvent:
|
|
31
|
+
"""Represents a subagent event for UI display."""
|
|
32
|
+
|
|
33
|
+
event_type: str # subagent_start, subagent_tool_call, subagent_complete
|
|
34
|
+
subagent_name: str
|
|
35
|
+
tool_name: Optional[str] = None
|
|
36
|
+
tool_args: Optional[Dict[str, Any]] = None
|
|
37
|
+
description: Optional[str] = None
|
|
38
|
+
result_preview: Optional[str] = None
|
|
39
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
40
|
+
|
|
41
|
+
def to_debug_message(self) -> str:
|
|
42
|
+
"""Convert to debug message for UI display (legacy, for logging)."""
|
|
43
|
+
if self.event_type == "subagent_start":
|
|
44
|
+
desc_preview = self.description[:80] + "..." if self.description and len(self.description) > 80 else self.description
|
|
45
|
+
return f"Subagent - {self.subagent_name} - 작업 시작: {desc_preview}"
|
|
46
|
+
elif self.event_type == "subagent_tool_call":
|
|
47
|
+
return f"Subagent - {self.subagent_name} - Tool 실행: {self.tool_name}"
|
|
48
|
+
elif self.event_type == "subagent_complete":
|
|
49
|
+
return f"Subagent - {self.subagent_name} - 완료"
|
|
50
|
+
else:
|
|
51
|
+
return f"Subagent - {self.subagent_name} - {self.event_type}"
|
|
52
|
+
|
|
53
|
+
def to_status_dict(self) -> Dict[str, Any]:
|
|
54
|
+
"""Convert to status dict with icon for SSE streaming."""
|
|
55
|
+
if self.event_type == "subagent_start":
|
|
56
|
+
desc_preview = self.description[:80] + "..." if self.description and len(self.description) > 80 else self.description
|
|
57
|
+
return {
|
|
58
|
+
"status": f"Subagent - {self.subagent_name} - 작업 시작: {desc_preview}",
|
|
59
|
+
"icon": "subagentStart"
|
|
60
|
+
}
|
|
61
|
+
elif self.event_type == "subagent_tool_call":
|
|
62
|
+
return {
|
|
63
|
+
"status": f"Subagent - {self.subagent_name} - Tool 실행: {self.tool_name}",
|
|
64
|
+
"icon": "tool"
|
|
65
|
+
}
|
|
66
|
+
elif self.event_type == "subagent_complete":
|
|
67
|
+
return {
|
|
68
|
+
"status": f"Subagent - {self.subagent_name} - 완료",
|
|
69
|
+
"icon": "subagentComplete"
|
|
70
|
+
}
|
|
71
|
+
else:
|
|
72
|
+
return {
|
|
73
|
+
"status": f"Subagent - {self.subagent_name} - {self.event_type}",
|
|
74
|
+
"icon": "info"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def set_current_subagent(name: str) -> None:
|
|
79
|
+
"""Set the current subagent context (for tool call tracking)."""
|
|
80
|
+
_subagent_context.name = name
|
|
81
|
+
logger.debug(f"Subagent context set: {name}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_current_subagent() -> Optional[str]:
|
|
85
|
+
"""Get the current subagent name, if any."""
|
|
86
|
+
return getattr(_subagent_context, "name", None)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def clear_current_subagent() -> None:
|
|
90
|
+
"""Clear the current subagent context."""
|
|
91
|
+
_subagent_context.name = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def emit_subagent_event(event: SubagentEvent) -> None:
|
|
95
|
+
"""Emit a subagent event to the queue."""
|
|
96
|
+
_event_queue.put(event)
|
|
97
|
+
logger.info(f"Subagent event: {event.to_debug_message()}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def emit_subagent_start(name: str, description: str) -> None:
|
|
101
|
+
"""Emit a subagent start event."""
|
|
102
|
+
emit_subagent_event(SubagentEvent(
|
|
103
|
+
event_type="subagent_start",
|
|
104
|
+
subagent_name=name,
|
|
105
|
+
description=description,
|
|
106
|
+
))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def emit_subagent_tool_call(
|
|
110
|
+
tool_name: str,
|
|
111
|
+
tool_args: Optional[Dict] = None,
|
|
112
|
+
subagent_name: Optional[str] = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Emit a subagent tool call event.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
tool_name: Name of the tool being called
|
|
119
|
+
tool_args: Optional arguments passed to the tool
|
|
120
|
+
subagent_name: Explicit subagent name (fallback to thread-local context)
|
|
121
|
+
"""
|
|
122
|
+
# Use explicit subagent_name or fall back to thread-local context
|
|
123
|
+
name = subagent_name or get_current_subagent()
|
|
124
|
+
|
|
125
|
+
if name:
|
|
126
|
+
emit_subagent_event(SubagentEvent(
|
|
127
|
+
event_type="subagent_tool_call",
|
|
128
|
+
subagent_name=name,
|
|
129
|
+
tool_name=tool_name,
|
|
130
|
+
tool_args=tool_args,
|
|
131
|
+
))
|
|
132
|
+
else:
|
|
133
|
+
# Log warning but still emit with generic name for visibility
|
|
134
|
+
logger.warning(f"No subagent context for tool call: {tool_name}")
|
|
135
|
+
emit_subagent_event(SubagentEvent(
|
|
136
|
+
event_type="subagent_tool_call",
|
|
137
|
+
subagent_name="subagent", # Generic fallback
|
|
138
|
+
tool_name=tool_name,
|
|
139
|
+
tool_args=tool_args,
|
|
140
|
+
))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def emit_subagent_complete(name: str, result_preview: Optional[str] = None) -> None:
|
|
144
|
+
"""Emit a subagent complete event."""
|
|
145
|
+
emit_subagent_event(SubagentEvent(
|
|
146
|
+
event_type="subagent_complete",
|
|
147
|
+
subagent_name=name,
|
|
148
|
+
result_preview=result_preview,
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def drain_subagent_events() -> List[SubagentEvent]:
|
|
153
|
+
"""
|
|
154
|
+
Drain all pending subagent events from the queue.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of SubagentEvent objects
|
|
158
|
+
"""
|
|
159
|
+
events = []
|
|
160
|
+
while True:
|
|
161
|
+
try:
|
|
162
|
+
event = _event_queue.get_nowait()
|
|
163
|
+
events.append(event)
|
|
164
|
+
except Empty:
|
|
165
|
+
break
|
|
166
|
+
return events
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_pending_event_count() -> int:
|
|
170
|
+
"""Get the number of pending events in the queue."""
|
|
171
|
+
return _event_queue.qsize()
|