hdsp-jupyter-extension 2.0.10__py3-none-any.whl → 2.0.11__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/custom_middleware.py +95 -9
- agent_server/langchain/hitl_config.py +5 -0
- agent_server/langchain/llm_factory.py +1 -85
- agent_server/langchain/prompts.py +105 -128
- agent_server/prompts/file_action_prompts.py +8 -8
- agent_server/routers/langchain_agent.py +78 -12
- 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.11.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js → hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.58c1e128ba0b76f41f04.js +81 -77
- hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.58c1e128ba0b76f41f04.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.11.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.9da31d1134a53b0c4af5.js +3 -3
- jupyter_ext/labextension/static/remoteEntry.4a252df3ade74efee8d6.js.map → hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.9da31d1134a53b0c4af5.js.map +1 -1
- {hdsp_jupyter_extension-2.0.10.dist-info → hdsp_jupyter_extension-2.0.11.dist-info}/METADATA +1 -1
- {hdsp_jupyter_extension-2.0.10.dist-info → hdsp_jupyter_extension-2.0.11.dist-info}/RECORD +68 -68
- jupyter_ext/__init__.py +21 -11
- jupyter_ext/_version.py +1 -1
- jupyter_ext/handlers.py +69 -50
- jupyter_ext/labextension/build_log.json +1 -1
- jupyter_ext/labextension/package.json +2 -2
- jupyter_ext/labextension/static/{lib_index_js.dc6434bee96ab03a0539.js → lib_index_js.58c1e128ba0b76f41f04.js} +81 -77
- jupyter_ext/labextension/static/lib_index_js.58c1e128ba0b76f41f04.js.map +1 -0
- jupyter_ext/labextension/static/{remoteEntry.4a252df3ade74efee8d6.js → remoteEntry.9da31d1134a53b0c4af5.js} +3 -3
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4a252df3ade74efee8d6.js.map → jupyter_ext/labextension/static/remoteEntry.9da31d1134a53b0c4af5.js.map +1 -1
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js.map +0 -1
- jupyter_ext/labextension/static/lib_index_js.dc6434bee96ab03a0539.js.map +0 -1
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.11.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.11.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js +0 -0
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +0 -0
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.11.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.11.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.11.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.11.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.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.11.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.11.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.11.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.11.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.11.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.11.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.11.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.11.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.11.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.11.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.11.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.11.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.11.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.10.dist-info → hdsp_jupyter_extension-2.0.11.dist-info}/licenses/LICENSE +0 -0
|
@@ -118,9 +118,9 @@ JSON만 응답하세요:"""
|
|
|
118
118
|
요청: {prompt}
|
|
119
119
|
|
|
120
120
|
분석 결과:
|
|
121
|
-
- 목표: {analysis[
|
|
122
|
-
- 라이브러리: {
|
|
123
|
-
- 분석 유형: {analysis[
|
|
121
|
+
- 목표: {analysis["objective"]}
|
|
122
|
+
- 라이브러리: {", ".join(analysis["libraries"])}
|
|
123
|
+
- 분석 유형: {analysis["analysis_type"]}
|
|
124
124
|
|
|
125
125
|
노트북의 각 셀에 대한 계획을 JSON 형식으로 작성해주세요:
|
|
126
126
|
{{
|
|
@@ -171,7 +171,7 @@ JSON만 응답하세요:"""
|
|
|
171
171
|
# Update progress (30% -> 80%)
|
|
172
172
|
progress = 30 + int((idx / total_cells) * 50)
|
|
173
173
|
self.task_manager.update_progress(
|
|
174
|
-
task_id, progress, f"셀 생성 중... ({idx+1}/{total_cells})"
|
|
174
|
+
task_id, progress, f"셀 생성 중... ({idx + 1}/{total_cells})"
|
|
175
175
|
)
|
|
176
176
|
|
|
177
177
|
cell_type = cell_plan.get("type", "code")
|
|
@@ -139,7 +139,8 @@ def create_handle_empty_response_middleware(wrap_model_call):
|
|
|
139
139
|
def handle_empty_response(request, handler):
|
|
140
140
|
max_retries = 2
|
|
141
141
|
|
|
142
|
-
# Check if all todos are completed - if so,
|
|
142
|
+
# Check if all todos are completed - if so, return empty response to stop agent
|
|
143
|
+
# Method 1: Check state.todos
|
|
143
144
|
todos = request.state.get("todos", [])
|
|
144
145
|
if todos:
|
|
145
146
|
pending_todos = [
|
|
@@ -147,10 +148,35 @@ def create_handle_empty_response_middleware(wrap_model_call):
|
|
|
147
148
|
]
|
|
148
149
|
if not pending_todos:
|
|
149
150
|
logger.info(
|
|
150
|
-
"All %d todos completed -
|
|
151
|
+
"All %d todos completed (from state) - stopping agent (no LLM call)",
|
|
151
152
|
len(todos),
|
|
152
153
|
)
|
|
153
|
-
return
|
|
154
|
+
return AIMessage(content="", tool_calls=[])
|
|
155
|
+
|
|
156
|
+
# Method 2: Check last message if it's a write_todos ToolMessage with all completed
|
|
157
|
+
messages = request.messages
|
|
158
|
+
if messages:
|
|
159
|
+
last_msg = messages[-1]
|
|
160
|
+
if getattr(last_msg, "type", "") == "tool":
|
|
161
|
+
tool_name = getattr(last_msg, "name", "") or ""
|
|
162
|
+
content = getattr(last_msg, "content", "") or ""
|
|
163
|
+
if tool_name == "write_todos" or "Updated todo list to" in content:
|
|
164
|
+
# Extract todos from ToolMessage content
|
|
165
|
+
try:
|
|
166
|
+
import ast
|
|
167
|
+
if "Updated todo list to" in content:
|
|
168
|
+
list_text = content.split("Updated todo list to", 1)[1].strip()
|
|
169
|
+
todos_from_msg = ast.literal_eval(list_text)
|
|
170
|
+
if isinstance(todos_from_msg, list) and len(todos_from_msg) > 0:
|
|
171
|
+
pending = [t for t in todos_from_msg if t.get("status") in ("pending", "in_progress")]
|
|
172
|
+
if not pending:
|
|
173
|
+
logger.info(
|
|
174
|
+
"All %d todos completed (from ToolMessage) - stopping agent (no LLM call)",
|
|
175
|
+
len(todos_from_msg),
|
|
176
|
+
)
|
|
177
|
+
return AIMessage(content="", tool_calls=[])
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.debug("Failed to parse todos from ToolMessage: %s", e)
|
|
154
180
|
|
|
155
181
|
# Check if last message is final_answer_tool result - if so, don't retry/synthesize
|
|
156
182
|
# This allows agent to naturally terminate after final_answer_tool
|
|
@@ -593,9 +619,17 @@ def create_normalize_tool_args_middleware(wrap_model_call, tools=None):
|
|
|
593
619
|
todos = args["todos"]
|
|
594
620
|
if isinstance(todos, list) and len(todos) > 0:
|
|
595
621
|
# Check if any todo contains summary keywords
|
|
596
|
-
summary_keywords = [
|
|
622
|
+
summary_keywords = [
|
|
623
|
+
"작업 요약",
|
|
624
|
+
"다음단계",
|
|
625
|
+
"다음 단계",
|
|
626
|
+
"요약 및",
|
|
627
|
+
]
|
|
597
628
|
has_summary = any(
|
|
598
|
-
any(
|
|
629
|
+
any(
|
|
630
|
+
kw in todo.get("content", "")
|
|
631
|
+
for kw in summary_keywords
|
|
632
|
+
)
|
|
599
633
|
for todo in todos
|
|
600
634
|
if isinstance(todo, dict)
|
|
601
635
|
)
|
|
@@ -604,7 +638,7 @@ def create_normalize_tool_args_middleware(wrap_model_call, tools=None):
|
|
|
604
638
|
# Add summary todo as last item
|
|
605
639
|
summary_todo = {
|
|
606
640
|
"content": "작업 요약 및 다음단계 제시",
|
|
607
|
-
"status": "pending"
|
|
641
|
+
"status": "pending",
|
|
608
642
|
}
|
|
609
643
|
todos.append(summary_todo)
|
|
610
644
|
logger.info(
|
|
@@ -612,6 +646,37 @@ def create_normalize_tool_args_middleware(wrap_model_call, tools=None):
|
|
|
612
646
|
len(todos),
|
|
613
647
|
)
|
|
614
648
|
|
|
649
|
+
# Log warning if summary todo is completed without JSON (but don't block)
|
|
650
|
+
for todo in todos:
|
|
651
|
+
if not isinstance(todo, dict):
|
|
652
|
+
continue
|
|
653
|
+
content = todo.get("content", "")
|
|
654
|
+
status = todo.get("status", "")
|
|
655
|
+
is_summary_todo = any(
|
|
656
|
+
kw in content for kw in summary_keywords
|
|
657
|
+
)
|
|
658
|
+
if is_summary_todo and status == "completed":
|
|
659
|
+
# Check if AIMessage content has summary JSON
|
|
660
|
+
msg_content = getattr(msg, "content", "") or ""
|
|
661
|
+
if isinstance(msg_content, list):
|
|
662
|
+
msg_content = " ".join(
|
|
663
|
+
str(p) for p in msg_content
|
|
664
|
+
)
|
|
665
|
+
has_summary_json = (
|
|
666
|
+
'"summary"' in msg_content
|
|
667
|
+
and '"next_items"' in msg_content
|
|
668
|
+
) or (
|
|
669
|
+
"'summary'" in msg_content
|
|
670
|
+
and "'next_items'" in msg_content
|
|
671
|
+
)
|
|
672
|
+
if not has_summary_json:
|
|
673
|
+
# Just log warning - don't block completion to avoid infinite loop
|
|
674
|
+
logger.warning(
|
|
675
|
+
"Summary todo marked completed but no summary JSON in content. "
|
|
676
|
+
"Allowing completion to proceed. Content: %s",
|
|
677
|
+
msg_content[:200],
|
|
678
|
+
)
|
|
679
|
+
|
|
615
680
|
return response
|
|
616
681
|
|
|
617
682
|
return normalize_tool_args
|
|
@@ -651,18 +716,39 @@ def create_inject_continuation_middleware(wrap_model_call):
|
|
|
651
716
|
pass
|
|
652
717
|
|
|
653
718
|
if tool_name in NON_HITL_TOOLS:
|
|
719
|
+
# Method 1: Check state.todos
|
|
654
720
|
todos = request.state.get("todos", [])
|
|
655
721
|
pending_todos = [
|
|
656
722
|
t for t in todos if t.get("status") in ("pending", "in_progress")
|
|
657
723
|
]
|
|
658
724
|
|
|
659
|
-
# If all todos are completed,
|
|
725
|
+
# If all todos are completed, DON'T call LLM - return empty response to stop agent
|
|
660
726
|
if not pending_todos and todos:
|
|
661
727
|
logger.info(
|
|
662
|
-
"All todos completed
|
|
728
|
+
"All todos completed (from state) after tool: %s - stopping agent (no LLM call)",
|
|
663
729
|
tool_name,
|
|
664
730
|
)
|
|
665
|
-
return
|
|
731
|
+
return AIMessage(content="", tool_calls=[])
|
|
732
|
+
|
|
733
|
+
# Method 2: Check ToolMessage content for write_todos
|
|
734
|
+
if tool_name == "write_todos" or "Updated todo list to" in (last_msg.content or ""):
|
|
735
|
+
try:
|
|
736
|
+
import ast
|
|
737
|
+
content = last_msg.content or ""
|
|
738
|
+
if "Updated todo list to" in content:
|
|
739
|
+
list_text = content.split("Updated todo list to", 1)[1].strip()
|
|
740
|
+
todos_from_msg = ast.literal_eval(list_text)
|
|
741
|
+
if isinstance(todos_from_msg, list) and len(todos_from_msg) > 0:
|
|
742
|
+
pending = [t for t in todos_from_msg if t.get("status") in ("pending", "in_progress")]
|
|
743
|
+
if not pending:
|
|
744
|
+
logger.info(
|
|
745
|
+
"All %d todos completed (from ToolMessage) after tool: %s - stopping agent",
|
|
746
|
+
len(todos_from_msg),
|
|
747
|
+
tool_name,
|
|
748
|
+
)
|
|
749
|
+
return AIMessage(content="", tool_calls=[])
|
|
750
|
+
except Exception as e:
|
|
751
|
+
logger.debug("Failed to parse todos from ToolMessage: %s", e)
|
|
666
752
|
|
|
667
753
|
logger.info(
|
|
668
754
|
"Injecting continuation prompt after non-HITL tool: %s",
|
|
@@ -58,6 +58,11 @@ def get_hitl_interrupt_config() -> Dict[str, Any]:
|
|
|
58
58
|
"allowed_decisions": ["approve", "edit", "reject"],
|
|
59
59
|
"description": "File edit requires approval",
|
|
60
60
|
},
|
|
61
|
+
# Multi-edit requires approval (multiple string replacements atomically)
|
|
62
|
+
"multiedit_file_tool": {
|
|
63
|
+
"allowed_decisions": ["approve", "edit", "reject"],
|
|
64
|
+
"description": "Multi-edit requires approval",
|
|
65
|
+
},
|
|
61
66
|
# Final answer doesn't need approval
|
|
62
67
|
"final_answer_tool": False,
|
|
63
68
|
}
|
|
@@ -4,7 +4,6 @@ LLM Factory for LangChain agent.
|
|
|
4
4
|
Provides functions to create LangChain LLM instances from configuration.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import json
|
|
8
7
|
import logging
|
|
9
8
|
from typing import Any, Dict
|
|
10
9
|
|
|
@@ -13,74 +12,6 @@ from agent_server.langchain.logging_utils import LLMTraceLogger
|
|
|
13
12
|
logger = logging.getLogger(__name__)
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
def _stringify_content(content: Any) -> str:
|
|
17
|
-
if content is None:
|
|
18
|
-
return ""
|
|
19
|
-
if isinstance(content, list):
|
|
20
|
-
parts = []
|
|
21
|
-
for part in content:
|
|
22
|
-
if isinstance(part, str):
|
|
23
|
-
parts.append(part)
|
|
24
|
-
elif isinstance(part, dict):
|
|
25
|
-
if part.get("type") == "text":
|
|
26
|
-
parts.append(part.get("text", ""))
|
|
27
|
-
elif "text" in part:
|
|
28
|
-
parts.append(part.get("text", ""))
|
|
29
|
-
return "\n".join(p for p in parts if p)
|
|
30
|
-
return str(content)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _summarize_payload(
|
|
34
|
-
payload: Dict[str, Any],
|
|
35
|
-
max_preview: int = 200,
|
|
36
|
-
max_items: int = 12,
|
|
37
|
-
) -> Dict[str, Any]:
|
|
38
|
-
summary: Dict[str, Any] = {}
|
|
39
|
-
|
|
40
|
-
def summarize_messages(key: str) -> None:
|
|
41
|
-
items = payload.get(key)
|
|
42
|
-
if not isinstance(items, list):
|
|
43
|
-
return
|
|
44
|
-
summarized = []
|
|
45
|
-
for item in items[:max_items]:
|
|
46
|
-
if not isinstance(item, dict):
|
|
47
|
-
summarized.append({"type": type(item).__name__})
|
|
48
|
-
continue
|
|
49
|
-
content_text = _stringify_content(item.get("content", ""))
|
|
50
|
-
entry = {
|
|
51
|
-
"role": item.get("role"),
|
|
52
|
-
"content_len": len(content_text),
|
|
53
|
-
"content_preview": content_text[:max_preview],
|
|
54
|
-
}
|
|
55
|
-
tool_calls = item.get("tool_calls")
|
|
56
|
-
if isinstance(tool_calls, list):
|
|
57
|
-
entry["tool_calls"] = [
|
|
58
|
-
tc.get("function", {}).get("name") or tc.get("name")
|
|
59
|
-
for tc in tool_calls
|
|
60
|
-
if isinstance(tc, dict)
|
|
61
|
-
]
|
|
62
|
-
summarized.append(entry)
|
|
63
|
-
if len(items) > max_items:
|
|
64
|
-
summarized.append({"truncated": len(items) - max_items})
|
|
65
|
-
summary[key] = summarized
|
|
66
|
-
|
|
67
|
-
summarize_messages("messages")
|
|
68
|
-
summarize_messages("input")
|
|
69
|
-
|
|
70
|
-
tools = payload.get("tools")
|
|
71
|
-
if isinstance(tools, list):
|
|
72
|
-
summary["tools"] = [
|
|
73
|
-
tool.get("function", {}).get("name") or tool.get("name")
|
|
74
|
-
for tool in tools
|
|
75
|
-
if isinstance(tool, dict)
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
if "tool_choice" in payload:
|
|
79
|
-
summary["tool_choice"] = payload.get("tool_choice")
|
|
80
|
-
|
|
81
|
-
return summary
|
|
82
|
-
|
|
83
|
-
|
|
84
15
|
def create_llm(llm_config: Dict[str, Any]):
|
|
85
16
|
"""Create LangChain LLM from config.
|
|
86
17
|
|
|
@@ -168,22 +99,7 @@ def _create_vllm_llm(llm_config: Dict[str, Any], callbacks):
|
|
|
168
99
|
|
|
169
100
|
logger.info(f"Creating vLLM LLM with model: {model}, endpoint: {endpoint}")
|
|
170
101
|
|
|
171
|
-
|
|
172
|
-
if "gpt-oss" in model.lower():
|
|
173
|
-
|
|
174
|
-
class ChatOpenAIGptOss(ChatOpenAI):
|
|
175
|
-
def _get_request_payload(self, input_, *, stop=None, **kwargs):
|
|
176
|
-
payload = super()._get_request_payload(input_, stop=stop, **kwargs)
|
|
177
|
-
summary = _summarize_payload(payload)
|
|
178
|
-
logger.info(
|
|
179
|
-
"gpt-oss payload summary: %s",
|
|
180
|
-
json.dumps(summary, ensure_ascii=False),
|
|
181
|
-
)
|
|
182
|
-
return payload
|
|
183
|
-
|
|
184
|
-
llm_class = ChatOpenAIGptOss
|
|
185
|
-
|
|
186
|
-
return llm_class(
|
|
102
|
+
return ChatOpenAI(
|
|
187
103
|
model=model,
|
|
188
104
|
api_key=api_key,
|
|
189
105
|
base_url=f"{endpoint}/v1",
|
|
@@ -5,142 +5,119 @@ Contains system prompts, JSON schema for fallback tool calling,
|
|
|
5
5
|
and middleware-specific prompts.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
DEFAULT_SYSTEM_PROMPT = """You are an expert Python data scientist and Jupyter notebook assistant.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- Output
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
- `
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
**When to paginate (use offset/limit):**
|
|
67
|
-
- Reading any file >500 lines
|
|
68
|
-
- Exploring unfamiliar codebases (always start with limit=100)
|
|
69
|
-
- Reading multiple files in sequence
|
|
70
|
-
- Any research or investigation task
|
|
71
|
-
|
|
72
|
-
**When full read is OK:**
|
|
73
|
-
- Small files (<500 lines)
|
|
74
|
-
- Files you need to edit immediately after reading
|
|
75
|
-
- After confirming file size with first scan
|
|
76
|
-
|
|
77
|
-
## 🔧 Code Development
|
|
78
|
-
For code generation/refactoring, use LSP tools (diagnostics_tool, references_tool) to check errors and find symbol usages. Use multiedit_file_tool for multiple changes in one file.
|
|
8
|
+
DEFAULT_SYSTEM_PROMPT = """You are an expert Python data scientist and Jupyter notebook assistant. Respond in Korean only.
|
|
9
|
+
|
|
10
|
+
# Core Rules
|
|
11
|
+
1. Be concise (≤4 lines unless detail requested)
|
|
12
|
+
2. ALWAYS call a tool in every response - never respond with text only
|
|
13
|
+
3. ALWAYS include a brief Korean explanation before tool calls
|
|
14
|
+
|
|
15
|
+
# Task Workflow
|
|
16
|
+
|
|
17
|
+
## Simple Tasks (1-2 steps)
|
|
18
|
+
Execute directly without todos.
|
|
19
|
+
|
|
20
|
+
## Complex Tasks (3+ steps)
|
|
21
|
+
1. Create todos with write_todos (all items in Korean)
|
|
22
|
+
2. ALWAYS include "작업 요약 및 다음단계 제시" as the LAST item
|
|
23
|
+
3. After each tool result: check todos → call next tool → repeat
|
|
24
|
+
4. **Final todo ("작업 요약 및 다음단계 제시")**:
|
|
25
|
+
- FIRST: Output summary JSON in your content (REQUIRED!)
|
|
26
|
+
- THEN: Call write_todos to mark all as completed
|
|
27
|
+
- Both must be in the SAME response
|
|
28
|
+
|
|
29
|
+
### Summary JSON Format (MUST output before marking complete)
|
|
30
|
+
```json
|
|
31
|
+
{"summary": "실행된 작업 요약", "next_items": [{"subject": "제목", "description": "설명"}]}
|
|
32
|
+
```
|
|
33
|
+
Suggest 3-5 next items. **You CANNOT mark "작업 요약" as completed without outputting this JSON first.**
|
|
34
|
+
|
|
35
|
+
# Mandatory Checks
|
|
36
|
+
|
|
37
|
+
## Resource Check (BEFORE data operations)
|
|
38
|
+
Call `check_resource_tool` FIRST when:
|
|
39
|
+
- Loading files (.csv, .parquet, .json, .xlsx, .pickle, .h5, .feather)
|
|
40
|
+
- Using pandas/polars/dask for dataframes
|
|
41
|
+
- Training ML models
|
|
42
|
+
|
|
43
|
+
# Tool Usage
|
|
44
|
+
|
|
45
|
+
## File Search (execute_command_tool)
|
|
46
|
+
```bash
|
|
47
|
+
find . -iname '*filename*.csv' 2>/dev/null # Find by name
|
|
48
|
+
grep -rn 'pattern' --include='*.py' . # Search contents
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## File Reading (read_file_tool)
|
|
52
|
+
- Large files: `read_file_tool(path, limit=100)` first
|
|
53
|
+
- Use `offset` for pagination
|
|
54
|
+
- Small files (<500 lines): Read without limit
|
|
55
|
+
|
|
56
|
+
## Code Output
|
|
57
|
+
- For plots/charts: Use English labels only
|
|
58
|
+
- Use LSP tools for error checking and symbol lookup
|
|
59
|
+
- Use multiedit_file_tool for multiple changes
|
|
60
|
+
|
|
61
|
+
# Forbidden
|
|
62
|
+
- Empty responses (no tool call AND no content)
|
|
63
|
+
- Tool calls without Korean explanation
|
|
64
|
+
- Stopping with pending/in_progress todos
|
|
79
65
|
"""
|
|
80
66
|
|
|
81
|
-
JSON_TOOL_SCHEMA = """
|
|
82
|
-
{
|
|
83
|
-
"tool": "<tool_name>",
|
|
84
|
-
"arguments": {"arg1": "value1", ...}
|
|
85
|
-
}
|
|
67
|
+
JSON_TOOL_SCHEMA = """Respond with ONLY valid JSON:
|
|
68
|
+
{"tool": "<name>", "arguments": {...}}
|
|
86
69
|
|
|
87
|
-
|
|
88
|
-
- jupyter_cell_tool:
|
|
89
|
-
- markdown_tool:
|
|
90
|
-
- write_todos:
|
|
91
|
-
- read_file_tool:
|
|
92
|
-
- write_file_tool:
|
|
93
|
-
- search_notebook_cells_tool:
|
|
94
|
-
- execute_command_tool:
|
|
95
|
-
- check_resource_tool:
|
|
70
|
+
Tools:
|
|
71
|
+
- jupyter_cell_tool: {"code": "<python>"}
|
|
72
|
+
- markdown_tool: {"content": "<markdown>"}
|
|
73
|
+
- write_todos: {"todos": [{"content": "한국어 내용", "status": "pending|in_progress|completed"}]}
|
|
74
|
+
- read_file_tool: {"path": "<path>", "offset": 0, "limit": 500}
|
|
75
|
+
- write_file_tool: {"path": "<path>", "content": "<content>", "overwrite": false}
|
|
76
|
+
- search_notebook_cells_tool: {"pattern": "<regex>"}
|
|
77
|
+
- execute_command_tool: {"command": "<cmd>"}
|
|
78
|
+
- check_resource_tool: {"files": ["<path>"], "dataframes": ["<var>"]}
|
|
96
79
|
|
|
97
|
-
|
|
80
|
+
No markdown wrapping. JSON only."""
|
|
98
81
|
|
|
99
82
|
TODO_LIST_SYSTEM_PROMPT = """
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
##
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
❌ Updating todo status without doing the actual work
|
|
134
|
-
❌ Marking a todo as 'completed' without actually executing it in THIS response
|
|
135
|
-
❌ Creating todo list WITHOUT "작업 요약 및 다음단계 제시" as final item
|
|
136
|
-
❌ Skipping ANY todo because a similar one was completed in PREVIOUS messages - past history ≠ current work
|
|
137
|
-
❌ Assuming work is done based on chat history - you must ALWAYS execute todos yourself
|
|
83
|
+
# Todo Rules
|
|
84
|
+
|
|
85
|
+
## New Message = Fresh Start
|
|
86
|
+
- Each user message is a NEW task
|
|
87
|
+
- Ignore completion status from chat history
|
|
88
|
+
- Execute ALL current todos from scratch
|
|
89
|
+
|
|
90
|
+
## Structure
|
|
91
|
+
All todo items must be in Korean. Always end with:
|
|
92
|
+
- 작업 요약 및 다음단계 제시 ← 필수 마지막 항목!
|
|
93
|
+
|
|
94
|
+
## Workflow
|
|
95
|
+
1. Find pending/in_progress todo
|
|
96
|
+
2. Execute it NOW in THIS response
|
|
97
|
+
3. Mark completed
|
|
98
|
+
4. Repeat until all done
|
|
99
|
+
|
|
100
|
+
## 🔴 Final Todo ("작업 요약 및 다음단계 제시") - CRITICAL
|
|
101
|
+
When executing this todo, you MUST:
|
|
102
|
+
1. Output the summary JSON in your content FIRST:
|
|
103
|
+
{"summary": "작업 내용 요약", "next_items": [{"subject": "...", "description": "..."}]}
|
|
104
|
+
2. THEN call write_todos to mark all as completed
|
|
105
|
+
3. If you don't output the JSON, the todo will NOT be marked as completed!
|
|
106
|
+
|
|
107
|
+
## Completion Check
|
|
108
|
+
- ✅ Done: Executed in THIS response
|
|
109
|
+
- ❌ Not done: Only visible in chat history
|
|
110
|
+
- ❌ "작업 요약" cannot be completed without outputting summary JSON
|
|
111
|
+
|
|
112
|
+
## Forbidden
|
|
113
|
+
- Marking complete without executing in THIS response
|
|
114
|
+
- Marking "작업 요약" complete without outputting JSON summary
|
|
115
|
+
- Todos without "작업 요약 및 다음단계 제시" as final item
|
|
138
116
|
"""
|
|
139
117
|
|
|
140
|
-
TODO_LIST_TOOL_DESCRIPTION = """Update
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
- If ALL todos are 'completed': output summary text BEFORE calling this tool, then call this to end the session"""
|
|
118
|
+
TODO_LIST_TOOL_DESCRIPTION = """Update task list for tracking progress.
|
|
119
|
+
This tool ONLY tracks status - does NOT execute tasks.
|
|
120
|
+
After calling: immediately call next action tool (unless ALL completed)."""
|
|
144
121
|
|
|
145
122
|
# Non-HITL tools that execute immediately without user approval
|
|
146
123
|
NON_HITL_TOOLS = {
|
|
@@ -20,9 +20,9 @@ def format_file_fix_prompt(
|
|
|
20
20
|
for rf in related_files:
|
|
21
21
|
if rf.get("content"):
|
|
22
22
|
related_context += f"""
|
|
23
|
-
### {rf[
|
|
23
|
+
### {rf["path"]}
|
|
24
24
|
```python
|
|
25
|
-
{rf[
|
|
25
|
+
{rf["content"]}
|
|
26
26
|
```
|
|
27
27
|
"""
|
|
28
28
|
|
|
@@ -33,9 +33,9 @@ def format_file_fix_prompt(
|
|
|
33
33
|
{error_output}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
## 메인 파일: {main_file[
|
|
36
|
+
## 메인 파일: {main_file["path"]}
|
|
37
37
|
```python
|
|
38
|
-
{main_file[
|
|
38
|
+
{main_file["content"]}
|
|
39
39
|
```
|
|
40
40
|
{related_context}
|
|
41
41
|
|
|
@@ -97,17 +97,17 @@ def format_file_custom_prompt(
|
|
|
97
97
|
for rf in related_files:
|
|
98
98
|
if rf.get("content"):
|
|
99
99
|
related_context += f"""
|
|
100
|
-
### {rf[
|
|
100
|
+
### {rf["path"]}
|
|
101
101
|
```python
|
|
102
|
-
{rf[
|
|
102
|
+
{rf["content"]}
|
|
103
103
|
```
|
|
104
104
|
"""
|
|
105
105
|
|
|
106
106
|
return f"""{custom_prompt}
|
|
107
107
|
|
|
108
|
-
## 메인 파일: {main_file[
|
|
108
|
+
## 메인 파일: {main_file["path"]}
|
|
109
109
|
```python
|
|
110
|
-
{main_file[
|
|
110
|
+
{main_file["content"]}
|
|
111
111
|
```
|
|
112
112
|
{related_context}
|
|
113
113
|
"""
|