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
@@ -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()