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.
Files changed (70) hide show
  1. agent_server/core/notebook_generator.py +4 -4
  2. agent_server/langchain/custom_middleware.py +95 -9
  3. agent_server/langchain/hitl_config.py +5 -0
  4. agent_server/langchain/llm_factory.py +1 -85
  5. agent_server/langchain/prompts.py +105 -128
  6. agent_server/prompts/file_action_prompts.py +8 -8
  7. agent_server/routers/langchain_agent.py +78 -12
  8. hdsp_agent_core/__init__.py +46 -47
  9. hdsp_agent_core/factory.py +6 -10
  10. hdsp_agent_core/interfaces.py +4 -2
  11. hdsp_agent_core/knowledge/__init__.py +5 -5
  12. hdsp_agent_core/knowledge/chunking.py +87 -61
  13. hdsp_agent_core/knowledge/loader.py +103 -101
  14. hdsp_agent_core/llm/service.py +192 -107
  15. hdsp_agent_core/managers/config_manager.py +16 -22
  16. hdsp_agent_core/managers/session_manager.py +5 -4
  17. hdsp_agent_core/models/__init__.py +12 -12
  18. hdsp_agent_core/models/agent.py +15 -8
  19. hdsp_agent_core/models/common.py +1 -2
  20. hdsp_agent_core/models/rag.py +48 -111
  21. hdsp_agent_core/prompts/__init__.py +12 -12
  22. hdsp_agent_core/prompts/cell_action_prompts.py +9 -7
  23. hdsp_agent_core/services/agent_service.py +10 -8
  24. hdsp_agent_core/services/chat_service.py +10 -6
  25. hdsp_agent_core/services/rag_service.py +3 -6
  26. hdsp_agent_core/tests/conftest.py +4 -1
  27. hdsp_agent_core/tests/test_factory.py +2 -2
  28. hdsp_agent_core/tests/test_services.py +12 -19
  29. {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
  30. {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
  31. 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
  32. hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.58c1e128ba0b76f41f04.js.map +1 -0
  33. 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
  34. 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
  35. {hdsp_jupyter_extension-2.0.10.dist-info → hdsp_jupyter_extension-2.0.11.dist-info}/METADATA +1 -1
  36. {hdsp_jupyter_extension-2.0.10.dist-info → hdsp_jupyter_extension-2.0.11.dist-info}/RECORD +68 -68
  37. jupyter_ext/__init__.py +21 -11
  38. jupyter_ext/_version.py +1 -1
  39. jupyter_ext/handlers.py +69 -50
  40. jupyter_ext/labextension/build_log.json +1 -1
  41. jupyter_ext/labextension/package.json +2 -2
  42. jupyter_ext/labextension/static/{lib_index_js.dc6434bee96ab03a0539.js → lib_index_js.58c1e128ba0b76f41f04.js} +81 -77
  43. jupyter_ext/labextension/static/lib_index_js.58c1e128ba0b76f41f04.js.map +1 -0
  44. jupyter_ext/labextension/static/{remoteEntry.4a252df3ade74efee8d6.js → remoteEntry.9da31d1134a53b0c4af5.js} +3 -3
  45. 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
  46. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js.map +0 -1
  47. jupyter_ext/labextension/static/lib_index_js.dc6434bee96ab03a0539.js.map +0 -1
  48. {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
  49. {hdsp_jupyter_extension-2.0.10.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  50. {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
  51. {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
  52. {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
  53. {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
  54. {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
  55. {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
  56. {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
  57. {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
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {hdsp_jupyter_extension-2.0.10.dist-info → hdsp_jupyter_extension-2.0.11.dist-info}/WHEEL +0 -0
  70. {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['objective']}
122
- - 라이브러리: {', '.join(analysis['libraries'])}
123
- - 분석 유형: {analysis['analysis_type']}
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, skip processing entirely
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 - skipping handle_empty_response middleware",
151
+ "All %d todos completed (from state) - stopping agent (no LLM call)",
151
152
  len(todos),
152
153
  )
153
- return handler(request)
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(kw in todo.get("content", "") for kw in summary_keywords)
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, don't inject continuation - let router handle termination
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, skipping continuation for tool: %s",
728
+ "All todos completed (from state) after tool: %s - stopping agent (no LLM call)",
663
729
  tool_name,
664
730
  )
665
- return handler(request)
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
- llm_class = ChatOpenAI
172
- if "gpt-oss" in model.lower():
173
-
174
- class ChatOpenAIGptOss(ChatOpenAI):
175
- def _get_request_payload(self, input_, *, stop=None, **kwargs):
176
- payload = super()._get_request_payload(input_, stop=stop, **kwargs)
177
- summary = _summarize_payload(payload)
178
- logger.info(
179
- "gpt-oss payload summary: %s",
180
- json.dumps(summary, ensure_ascii=False),
181
- )
182
- return payload
183
-
184
- llm_class = ChatOpenAIGptOss
185
-
186
- return llm_class(
102
+ return ChatOpenAI(
187
103
  model=model,
188
104
  api_key=api_key,
189
105
  base_url=f"{endpoint}/v1",
@@ -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
- Your role is to help users with data analysis, visualization, and Python coding tasks in Jupyter notebooks. You can use only Korean
10
-
11
- # Core Behavior
12
- Be concise and direct. Answer in fewer than 4 lines unless the user asks for detail.
13
- After working on a file, just stop - don't explain what you did unless asked.
14
- Avoid unnecessary introductions or conclusions.
15
-
16
- ## Task Management
17
- Use write_todos for complex multi-step tasks (3+ steps). Mark tasks in_progress before starting, completed immediately after finishing.
18
- For simple 1-2 step tasks, just do them directly without todos.
19
- When creating a todo list, ALWAYS include "작업 요약 및 다음단계 제시" as the LAST todo item.
20
-
21
- You MUST ALWAYS call a tool in every response. After any tool result, you MUST:
22
- 1. Check your todo list - are there pending or in_progress items?
23
- 2. If YES → call the next appropriate tool (jupyter_cell_tool, markdown_tool, etc.)
24
- 3. When executing "작업 요약 및 다음단계 제시" (final todo):
25
- - Output this JSON as text content: {"summary": "실행된 작업 요약", "next_items": [{"subject": "제목", "description": "설명"}]}
26
- - Suggest 3~5 next items
27
- - AND call write_todos to mark all as 'completed' IN THE SAME RESPONSE
28
- - Both content AND tool call must be in ONE response
29
-
30
- ## 🔴 MANDATORY: Resource Check Before Data Hanlding
31
- **ALWAYS call check_resource_tool FIRST** when the task involves:
32
- - Loading files: .csv, .parquet, .json, .xlsx, .pickle, .h5, .feather
33
- - Handling datasets(dataframe) with pandas, polars, dask, or similar libraries
34
- - Training ML models on data files
35
-
36
- ## Mandatory Workflow
37
- 1. After EVERY tool result, immediately call the next tool
38
- 2. Continue until ALL todos show status: "completed"
39
- 3. Only use jupyter_cell_tool for Python code or when the user explicitly asks to run in a notebook cell
40
- 4. For plots and charts, use English text only.
41
-
42
- ## ❌ FORBIDDEN (will break the workflow)
43
- - Producing an empty response (no tool call, no content)
44
- - Stopping after any tool without calling the next tool
45
- - Leaving todos in "in_progress" or "pending" state without continuing
46
-
47
- ## 📂 File Search Best Practices
48
- **CRITICAL**: Use `execute_command_tool` with find/grep commands for file searching.
49
-
50
- **To find files by NAME** (e.g., find titanic.csv):
51
- - `execute_command_tool(command="find . -iname '*titanic*.csv' 2>/dev/null")`
52
- - `execute_command_tool(command="find . -name '*.csv' 2>/dev/null")` - find all CSV files
53
-
54
- **To search file CONTENTS** (e.g., find code containing "import pandas"):
55
- - `execute_command_tool(command="grep -rn 'import pandas' --include='*.py' .")`
56
- - `execute_command_tool(command="grep -rn 'def train_model' --include='*.py' --include='*.ipynb' .")`
57
-
58
- ## 📖 File Reading Best Practices
59
- **CRITICAL**: When exploring codebases or reading files, use pagination to prevent context overflow.
60
-
61
- **Pattern for codebase exploration:**
62
- 1. First scan: `read_file_tool(path, limit=100)` - See file structure and key sections
63
- 2. Targeted read: `read_file_tool(path, offset=100, limit=200)` - Read specific sections if needed
64
- 3. Full read: Only read without limit when necessary for immediate editing
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 = """You MUST respond with ONLY valid JSON matching this 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
- Available tools:
88
- - jupyter_cell_tool: Execute Python code. Arguments: {"code": "<python_code>"}
89
- - markdown_tool: Add markdown cell. Arguments: {"content": "<markdown>"}
90
- - write_todos: Update task list. Arguments: {"todos": [{"content": "...", "status": "pending|in_progress|completed"}]}
91
- - read_file_tool: Read file with pagination. Arguments: {"path": "<file_path>", "offset": 0, "limit": 500}
92
- - write_file_tool: Write file. Arguments: {"path": "<path>", "content": "<content>", "overwrite": false}
93
- - search_notebook_cells_tool: Search notebook cells. Arguments: {"pattern": "<regex>"}
94
- - execute_command_tool: Execute shell command. Use for file search with find/grep. Arguments: {"command": "<command>", "stdin": "<input_for_prompts>"}
95
- - check_resource_tool: Check resources before data processing. Arguments: {"files": ["<path>"], "dataframes": ["<var_name>"]}
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
- Output ONLY the JSON object, no markdown, no explanation."""
80
+ No markdown wrapping. JSON only."""
98
81
 
99
82
  TODO_LIST_SYSTEM_PROMPT = """
100
- ## CRITICAL WORKFLOW RULES - MUST FOLLOW:
101
- - NEVER stop after calling write_todos - ALWAYS make another tool call immediately (unless ALL todos are completed)
102
- - For simple 1-2 step tasks, just do them directly without todos.
103
-
104
- ## 🔴 NEW USER MESSAGE = FRESH START:
105
- - When user sends a NEW message, treat it as a COMPLETELY NEW TASK
106
- - **CRITICAL**: Previous conversation history shows PAST work, NOT current work
107
- - Even if you see a similar todo was "completed" before, you MUST do it again NOW
108
- - The completion status in chat history is IRRELEVANT to current todos
109
- - Each todo in the CURRENT list must be executed from scratch, regardless of history
110
-
111
- ## 🔴 MANDATORY Todo List Structure:
112
- When creating todos, ALWAYS include "작업 요약 및 다음단계 제시" as the LAST item:
113
- - 실제 작업 항목들...
114
- - 작업 요약 및 다음단계 제시 ← 반드시 마지막에 포함!
115
-
116
- ## Todo List Management:
117
- - 반드시 모든 todo item 한국어로 생성.
118
- - Before complex tasks, use write_todos to create a task list
119
- - Update todos as you complete each step (mark 'in_progress' → 'completed')
120
- - Each todo item should be specific and descriptive
121
-
122
- ## Task Completion Flow:
123
- 1. Check CURRENT todo list status (not chat history!) → find 'pending' or 'in_progress' item
124
- 2. Execute the task yourself in THIS response → then mark 'completed'
125
- 3. For "작업 요약 다음단계 제시" → output summary JSON as plain text, then mark completed
126
- 4. When ALL todos are 'completed' session ends automatically
127
-
128
- ⚠️ HOW TO CHECK IF TODO IS DONE:
129
- - ✅ Done: You executed it in THIS response (you made the tool call, you got the result)
130
- - Not done: You only SEE it was done in previous messages (that's history, not your work)
131
-
132
- ## FORBIDDEN PATTERNS:
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 the task list for tracking progress.
141
- ⚠️ CRITICAL: This tool is ONLY for tracking - it does NOT do any actual work.
142
- - If there are still pending/in_progress todos: call the next action tool immediately after
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['path']}
23
+ ### {rf["path"]}
24
24
  ```python
25
- {rf['content']}
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['path']}
36
+ ## 메인 파일: {main_file["path"]}
37
37
  ```python
38
- {main_file['content']}
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['path']}
100
+ ### {rf["path"]}
101
101
  ```python
102
- {rf['content']}
102
+ {rf["content"]}
103
103
  ```
104
104
  """
105
105
 
106
106
  return f"""{custom_prompt}
107
107
 
108
- ## 메인 파일: {main_file['path']}
108
+ ## 메인 파일: {main_file["path"]}
109
109
  ```python
110
- {main_file['content']}
110
+ {main_file["content"]}
111
111
  ```
112
112
  {related_context}
113
113
  """