hdsp-jupyter-extension 2.0.25__py3-none-any.whl → 2.0.26__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 (43) hide show
  1. agent_server/langchain/agent_factory.py +14 -14
  2. agent_server/langchain/agent_prompts/planner_prompt.py +13 -19
  3. agent_server/langchain/custom_middleware.py +73 -17
  4. agent_server/langchain/models/gpt_oss_chat.py +26 -13
  5. agent_server/langchain/prompts.py +11 -8
  6. agent_server/langchain/tools/jupyter_tools.py +43 -0
  7. agent_server/routers/langchain_agent.py +198 -22
  8. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
  9. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
  10. hdsp_jupyter_extension-2.0.25.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.ffc2b4bc8e6cb300e1e1.js → hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.0fe2dcbbd176ee0efceb.js +2 -2
  11. hdsp_jupyter_extension-2.0.25.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.ffc2b4bc8e6cb300e1e1.js.map → hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.0fe2dcbbd176ee0efceb.js.map +1 -1
  12. {hdsp_jupyter_extension-2.0.25.dist-info → hdsp_jupyter_extension-2.0.26.dist-info}/METADATA +1 -1
  13. {hdsp_jupyter_extension-2.0.25.dist-info → hdsp_jupyter_extension-2.0.26.dist-info}/RECORD +43 -43
  14. jupyter_ext/_version.py +1 -1
  15. jupyter_ext/labextension/build_log.json +1 -1
  16. jupyter_ext/labextension/package.json +2 -2
  17. jupyter_ext/labextension/static/{remoteEntry.ffc2b4bc8e6cb300e1e1.js → remoteEntry.0fe2dcbbd176ee0efceb.js} +2 -2
  18. jupyter_ext/labextension/static/{remoteEntry.ffc2b4bc8e6cb300e1e1.js.map → remoteEntry.0fe2dcbbd176ee0efceb.js.map} +1 -1
  19. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
  20. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  21. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js +0 -0
  22. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js.map +0 -0
  23. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js +0 -0
  24. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js.map +0 -0
  25. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.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
  26. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.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
  27. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.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
  28. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.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
  29. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
  30. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.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
  31. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.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
  32. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +0 -0
  33. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.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
  34. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +0 -0
  35. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.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
  36. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
  37. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.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
  38. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
  39. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
  40. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +0 -0
  41. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -0
  42. {hdsp_jupyter_extension-2.0.25.dist-info → hdsp_jupyter_extension-2.0.26.dist-info}/WHEEL +0 -0
  43. {hdsp_jupyter_extension-2.0.25.dist-info → hdsp_jupyter_extension-2.0.26.dist-info}/licenses/LICENSE +0 -0
@@ -315,24 +315,24 @@ def create_main_agent(
315
315
  except Exception as e:
316
316
  logger.warning(f"Failed to add SummarizationMiddleware: {e}")
317
317
 
318
- # Build system prompt - FORCE default prompt for testing
319
- # TODO: Remove this override after frontend localStorage is cleared
320
- # Original priority: system_prompt_override > agent_prompts.planner > default
321
- # DEBUG: Log all prompt sources to find root cause of MALFORMED_FUNCTION_CALL
318
+ # Build system prompt with priority: system_prompt_override > agent_prompts.planner > default
322
319
  logger.info(
323
- "DEBUG Main Agent prompt sources: system_prompt_override=%s, "
324
- "agent_prompts.planner=%s, using=DEFAULT",
320
+ "Main Agent prompt sources: system_prompt_override=%s (len=%d), "
321
+ "agent_prompts.planner=%s",
325
322
  bool(system_prompt_override),
323
+ len(system_prompt_override) if system_prompt_override else 0,
326
324
  bool(agent_prompts.get("planner") if agent_prompts else None),
327
325
  )
328
- if agent_prompts:
329
- logger.info(
330
- "DEBUG: agent_prompts keys=%s, planner prompt length=%d",
331
- list(agent_prompts.keys()),
332
- len(agent_prompts.get("planner", "") or ""),
333
- )
334
- system_prompt = PLANNER_SYSTEM_PROMPT
335
- logger.info("Using PLANNER_SYSTEM_PROMPT (length=%d)", len(system_prompt))
326
+
327
+ if system_prompt_override and system_prompt_override.strip():
328
+ system_prompt = system_prompt_override.strip()
329
+ logger.info("Using system_prompt_override (length=%d)", len(system_prompt))
330
+ elif agent_prompts and agent_prompts.get("planner"):
331
+ system_prompt = agent_prompts["planner"]
332
+ logger.info("Using agent_prompts.planner (length=%d)", len(system_prompt))
333
+ else:
334
+ system_prompt = PLANNER_SYSTEM_PROMPT
335
+ logger.info("Using PLANNER_SYSTEM_PROMPT (length=%d)", len(system_prompt))
336
336
 
337
337
  # Log provider info for debugging
338
338
  provider = llm_config.get("provider", "")
@@ -55,25 +55,19 @@ PLANNER_SYSTEM_PROMPT = """당신은 작업을 조율하는 Main Agent입니다.
55
55
  - content에 도구(tool)명 언급 금지
56
56
  - **[필수] 마지막 todo는 반드시 "작업 요약 및 다음 단계 제시"**
57
57
 
58
- # "작업 요약 및 다음 단계 제시" todo 작업 순서 [필수]
59
- 1. "작업 요약 및 다음 단계 제시"를 **in_progress**로 변경 (write_todos 호출)
60
- 2. **같은 응답에서** 아래 JSON을 텍스트로 출력:
61
- {
62
- "summary": "완료된 작업 요약",
63
- "next_items": [
64
- {
65
- "subject": "제목",
66
- "description": "설명"
67
- }
68
- ]
69
- }
70
- 3. JSON 출력과 함께 "작업 요약 및 다음 단계 제시"를 **completed**로 변경
71
- **중요**: JSON은 반드시 in_progress 상태일 때 출력! completed 먼저 표시 금지!
72
- - next_items 3개 이상 필수
73
- - **summary JSON 없이 종료 금지**
74
- - **주의**: JSON은 todo 항목이 아닌 일반 텍스트 응답으로 출력
75
-
76
-
58
+ # "작업 요약 및 다음 단계 제시" todo 완료 [필수]
59
+ 1. "작업 요약 및 다음 단계 제시"를 **in_progress**로 변경 (write_todos 호출)
60
+ 2. **반드시 final_summary_tool 호출**:
61
+ final_summary_tool(
62
+ summary="완료된 작업 요약",
63
+ next_items=[{"subject": "제목", "description": "설명"}, ...]
64
+ )
65
+
66
+ 3. final_summary_tool 호출 후 "작업 요약 및 다음 단계 제시" **completed**로 변경
67
+
68
+ - next_items 3개 이상 필수
69
+ - **final_summary_tool 호출 없이 종료 금지**
70
+
77
71
  # 도구 사용시 주의할 점
78
72
 
79
73
  ## 서브에이전트 호출 (코드/쿼리 생성 시 필수)
@@ -165,6 +165,22 @@ def try_extract_tool_calls_from_additional_kwargs(
165
165
  if not raw_tool_calls:
166
166
  return None
167
167
 
168
+ # IMPORTANT: Only use the first tool_call to prevent parallel execution issues
169
+ # LLM sometimes generates multiple tool_calls despite prompt instructions
170
+ if len(raw_tool_calls) > 1:
171
+ first_tc = raw_tool_calls[0]
172
+ first_name = first_tc.get("function", {}).get("name", "unknown")
173
+ ignored_names = [
174
+ tc.get("function", {}).get("name", "unknown") for tc in raw_tool_calls[1:]
175
+ ]
176
+ logger.warning(
177
+ "Multiple tool_calls in additional_kwargs (%d), using only first one: %s. Ignored: %s",
178
+ len(raw_tool_calls),
179
+ first_name,
180
+ ignored_names,
181
+ )
182
+ raw_tool_calls = raw_tool_calls[:1]
183
+
168
184
  repaired_tool_calls = []
169
185
  for tc in raw_tool_calls:
170
186
  func = tc.get("function", {})
@@ -316,8 +332,7 @@ def create_handle_empty_response_middleware(wrap_model_call):
316
332
  summary_todo_completed = all_todos_completed and last_todo_is_summary
317
333
 
318
334
  if not summary_todo_completed and any(
319
- t.get("status") == "completed"
320
- and "작업 요약" in t.get("content", "")
335
+ t.get("status") == "completed" and "작업 요약" in t.get("content", "")
321
336
  for t in todos
322
337
  ):
323
338
  logger.debug(
@@ -439,15 +454,16 @@ def create_handle_empty_response_middleware(wrap_model_call):
439
454
  content = " ".join(str(p) for p in content)
440
455
 
441
456
  # Check if content contains summary JSON pattern
442
- has_summary_pattern = ('"summary"' in content or "'summary'" in content) and (
443
- '"next_items"' in content or "'next_items'" in content
444
- )
457
+ has_summary_pattern = (
458
+ '"summary"' in content or "'summary'" in content
459
+ ) and ('"next_items"' in content or "'next_items'" in content)
445
460
 
446
461
  if has_summary_pattern:
447
462
  # Check if pending todos exist - if so, don't force complete
448
463
  current_todos = request.state.get("todos", [])
449
464
  pending_todos = [
450
- t for t in current_todos
465
+ t
466
+ for t in current_todos
451
467
  if isinstance(t, dict) and t.get("status") == "pending"
452
468
  ]
453
469
  if pending_todos:
@@ -463,7 +479,12 @@ def create_handle_empty_response_middleware(wrap_model_call):
463
479
  try:
464
480
  # Try to find JSON object containing summary
465
481
  import re
466
- json_match = re.search(r'\{[^{}]*"summary"[^{}]*"next_items"[^{}]*\}', content, re.DOTALL)
482
+
483
+ json_match = re.search(
484
+ r'\{[^{}]*"summary"[^{}]*"next_items"[^{}]*\}',
485
+ content,
486
+ re.DOTALL,
487
+ )
467
488
  if json_match:
468
489
  repaired_summary = repair_json(
469
490
  json_match.group(), return_objects=True
@@ -503,7 +524,9 @@ def create_handle_empty_response_middleware(wrap_model_call):
503
524
  )
504
525
  return response
505
526
  except Exception as e:
506
- logger.debug(f"Failed to extract summary JSON from mixed content: {e}")
527
+ logger.debug(
528
+ f"Failed to extract summary JSON from mixed content: {e}"
529
+ )
507
530
 
508
531
  # Fallback: accept as-is if repair failed but looks like summary
509
532
  logger.info(
@@ -543,7 +566,9 @@ def create_handle_empty_response_middleware(wrap_model_call):
543
566
  is_summary_todo = any(
544
567
  kw in current_todo.get("content", "") for kw in summary_keywords
545
568
  )
546
- if is_summary_todo and ('"summary"' in content or "'summary'" in content):
569
+ if is_summary_todo and (
570
+ '"summary"' in content or "'summary'" in content
571
+ ):
547
572
  # This is a summary todo with summary content - accept it
548
573
  logger.info(
549
574
  "Summary todo with summary content detected - accepting"
@@ -785,7 +810,7 @@ def _create_synthetic_completion(request, response_message, has_content):
785
810
  logger.warning(
786
811
  "Force-completing %d pending todos that were never started: %s",
787
812
  pending_count,
788
- [t.get("content") for t in todos if t.get("status") == "pending"]
813
+ [t.get("content") for t in todos if t.get("status") == "pending"],
789
814
  )
790
815
 
791
816
  # Mark all todos as completed
@@ -858,6 +883,21 @@ def create_limit_tool_calls_middleware(wrap_model_call):
858
883
  )
859
884
  msg.tool_calls = [tool_calls[0]]
860
885
 
886
+ # Remove additional_kwargs["tool_calls"] entirely when
887
+ # msg.tool_calls exists. ChatOpenAI duplicates tool_calls
888
+ # into additional_kwargs, and leftover entries pollute the
889
+ # conversation context - LLM sees them and assumes all
890
+ # listed tool calls were executed.
891
+ additional_kwargs = getattr(msg, "additional_kwargs", {})
892
+ if msg.tool_calls and additional_kwargs.get("tool_calls"):
893
+ removed_count = len(additional_kwargs["tool_calls"])
894
+ del additional_kwargs["tool_calls"]
895
+ logger.info(
896
+ "Removed %d tool_calls from additional_kwargs "
897
+ "(canonical source: msg.tool_calls)",
898
+ removed_count,
899
+ )
900
+
861
901
  return response
862
902
 
863
903
  return limit_tool_calls_to_one
@@ -1026,29 +1066,45 @@ def create_normalize_tool_args_middleware(wrap_model_call, tools=None):
1026
1066
 
1027
1067
  # Validate: "작업 요약 및 다음 단계 제시" cannot be in_progress if pending todos exist
1028
1068
  # This prevents LLM from skipping pending tasks
1029
- summary_keywords = ["작업 요약", "다음 단계 제시"]
1069
+ summary_keywords = [
1070
+ "작업 요약",
1071
+ "다음 단계 제시",
1072
+ ]
1030
1073
  for i, todo in enumerate(todos):
1031
1074
  if not isinstance(todo, dict):
1032
1075
  continue
1033
1076
  content = todo.get("content", "")
1034
- is_summary_todo = any(kw in content for kw in summary_keywords)
1077
+ is_summary_todo = any(
1078
+ kw in content for kw in summary_keywords
1079
+ )
1035
1080
 
1036
- if is_summary_todo and todo.get("status") == "in_progress":
1081
+ if (
1082
+ is_summary_todo
1083
+ and todo.get("status") == "in_progress"
1084
+ ):
1037
1085
  # Check if there are pending todos before this one
1038
1086
  pending_before = [
1039
- t for t in todos[:i]
1040
- if isinstance(t, dict) and t.get("status") == "pending"
1087
+ t
1088
+ for t in todos[:i]
1089
+ if isinstance(t, dict)
1090
+ and t.get("status") == "pending"
1041
1091
  ]
1042
1092
  if pending_before:
1043
1093
  # Revert summary todo to pending
1044
1094
  todo["status"] = "pending"
1045
1095
  # Set the first pending todo to in_progress
1046
1096
  for t in todos:
1047
- if isinstance(t, dict) and t.get("status") == "pending":
1097
+ if (
1098
+ isinstance(t, dict)
1099
+ and t.get("status")
1100
+ == "pending"
1101
+ ):
1048
1102
  t["status"] = "in_progress"
1049
1103
  logger.warning(
1050
1104
  "Reverted summary todo to pending, set '%s' to in_progress (pending todos exist)",
1051
- t.get("content", "")[:30],
1105
+ t.get("content", "")[
1106
+ :30
1107
+ ],
1052
1108
  )
1053
1109
  break
1054
1110
  break
@@ -200,19 +200,20 @@ class ChatGPTOSS(BaseChatModel):
200
200
  tool_calls_list = []
201
201
 
202
202
  if message.tool_calls:
203
- additional_kwargs["tool_calls"] = [
204
- {
205
- "id": tc.id,
206
- "type": "function",
207
- "function": {
208
- "name": tc.function.name,
209
- "arguments": tc.function.arguments,
210
- },
211
- }
212
- for tc in message.tool_calls
213
- ]
214
- # Also convert to LangChain tool_calls format
215
- for tc in message.tool_calls:
203
+ # IMPORTANT: Only use the first tool_call to prevent parallel execution issues
204
+ # LLM sometimes generates multiple tool_calls despite prompt instructions
205
+ tool_calls_to_use = message.tool_calls[:1]
206
+ if len(message.tool_calls) > 1:
207
+ logger.warning(
208
+ "Multiple tool_calls detected (%d), using only first one: %s. Ignored: %s",
209
+ len(message.tool_calls),
210
+ message.tool_calls[0].function.name,
211
+ [tc.function.name for tc in message.tool_calls[1:]],
212
+ )
213
+
214
+ # Convert to LangChain tool_calls format only
215
+ # Do NOT put tool_calls in additional_kwargs to avoid duplicate/conflicting data
216
+ for tc in tool_calls_to_use:
216
217
  try:
217
218
  args = json.loads(tc.function.arguments)
218
219
  except json.JSONDecodeError:
@@ -327,9 +328,21 @@ class ChatGPTOSS(BaseChatModel):
327
328
  tool_call_chunks = []
328
329
 
329
330
  # Handle tool calls in streaming
331
+ # IMPORTANT: Only process the first tool_call (index 0) to prevent parallel execution
330
332
  if delta.tool_calls:
331
333
  for tc in delta.tool_calls:
332
334
  idx = tc.index
335
+ # Only accumulate the first tool_call (index 0)
336
+ if idx > 0:
337
+ if idx == 1 and tc.function and tc.function.name:
338
+ # Log warning only once when we first see index 1
339
+ logger.warning(
340
+ "Multiple tool_calls in stream, ignoring index %d: %s",
341
+ idx,
342
+ tc.function.name,
343
+ )
344
+ continue
345
+
333
346
  if idx not in tool_calls_accum:
334
347
  tool_calls_accum[idx] = {
335
348
  "id": tc.id or "",
@@ -31,12 +31,12 @@ DEFAULT_SYSTEM_PROMPT = """You are an expert Python data scientist and Jupyter n
31
31
  - **[중요] "작업 요약 및 다음 단계 제시"는 summary JSON 출력 후에만 completed 표시**
32
32
 
33
33
  # 모든 작업 완료 후 [필수]
34
- 마지막 todo "작업 요약 및 다음 단계 제시"를 completed로 변경한 후,
35
- **텍스트 응답으로** 아래 JSON을 출력하세요 (todo content가 아님!):
36
- {"summary": "완료된 작업 요약", "next_items": [{"subject": "제목", "description": "설명"}]}
34
+ 마지막 todo "작업 요약 및 다음 단계 제시"를 완료할 때:
35
+ **반드시 final_summary_tool을 호출**하여 요약과 다음 단계를 제시하세요.
36
+ - final_summary_tool(summary="완료된 작업 요약", next_items=[{"subject": "제목", "description": "설명"}, ...])
37
37
  - next_items 3개 이상 필수
38
- - **summary JSON 없이 종료 금지**
39
- - **주의**: JSON todo 항목이 아닌 일반 텍스트 응답으로 출력
38
+ - **final_summary_tool 호출 없이 종료 금지**
39
+ - **주의**: 텍스트로 JSON 출력하지 말고, 반드시 도구 호출로!
40
40
 
41
41
  # 도구 사용
42
42
  - check_resource_tool: 대용량 파일/데이터프레임 작업 전 필수
@@ -94,9 +94,9 @@ TODO_LIST_TOOL_DESCRIPTION = """Todo 리스트 관리 도구.
94
94
  - in_progress 상태는 **동시에 1개만** 허용
95
95
  - **[필수] 마지막 todo는 반드시 "작업 요약 및 다음 단계 제시"로 생성**
96
96
  - **🔴 [실행 순서]**: todo는 반드시 리스트 순서대로 실행하고, "작업 요약 및 다음 단계 제시"는 맨 마지막에 실행
97
- - 이 "작업 요약 및 다음 단계 제시" todo 에서는 전체 작업 요약과 다음 단계를 제시하는 내용을 JSON 형태로 출력:
98
- {"summary": "완료 요약", "next_items": [{"subject": "...", "description": "..."}]}
99
- (next_items 3개 이상 필수)
97
+ - 이 "작업 요약 및 다음 단계 제시" todo 완료 **반드시 final_summary_tool 호출**:
98
+ final_summary_tool(summary="완료 요약", next_items=[{"subject": "...", "description": "..."}])
99
+ (next_items 3개 이상 필수, 텍스트 JSON 출력 금지!)
100
100
  """
101
101
 
102
102
  # List of tools available to the agent
@@ -104,6 +104,7 @@ TOOL_LIST = [
104
104
  "jupyter_cell_tool",
105
105
  "markdown_tool",
106
106
  "ask_user_tool",
107
+ "final_summary_tool",
107
108
  "write_todos",
108
109
  "read_file_tool",
109
110
  "write_file_tool",
@@ -128,6 +129,8 @@ NON_HITL_TOOLS = {
128
129
  "search_notebook_cells_tool",
129
130
  "search_notebook_cells",
130
131
  "write_todos",
132
+ "final_summary_tool",
133
+ "final_summary",
131
134
  # LSP tools (read-only)
132
135
  "diagnostics_tool",
133
136
  "diagnostics",
@@ -172,9 +172,52 @@ def ask_user_tool(
172
172
  }
173
173
 
174
174
 
175
+ class NextItem(BaseModel):
176
+ """Schema for a next step item"""
177
+
178
+ subject: str = Field(description="다음 단계 제목")
179
+ description: str = Field(description="다음 단계 설명")
180
+
181
+
182
+ class FinalSummaryInput(BaseModel):
183
+ """Input schema for final_summary tool"""
184
+
185
+ summary: str = Field(description="완료된 작업에 대한 요약 (한국어)")
186
+ next_items: List[NextItem] = Field(
187
+ description="다음 단계 제안 목록 (3개 이상)", min_length=3
188
+ )
189
+
190
+
191
+ @tool(args_schema=FinalSummaryInput)
192
+ def final_summary_tool(
193
+ summary: str,
194
+ next_items: List[Dict[str, str]],
195
+ ) -> Dict[str, Any]:
196
+ """
197
+ 모든 작업이 완료된 후 최종 요약과 다음 단계를 제시하는 도구.
198
+
199
+ 이 도구는 반드시 모든 todo가 완료된 후, 마지막 "작업 요약 및 다음 단계 제시" todo를 처리할 때만 호출하세요.
200
+
201
+ Args:
202
+ summary: 완료된 작업에 대한 요약 (한국어로 작성)
203
+ next_items: 다음 단계 제안 목록 (각각 subject와 description 포함, 3개 이상)
204
+
205
+ Returns:
206
+ Dict containing the summary and next items for frontend display
207
+ """
208
+ return {
209
+ "tool": "final_summary",
210
+ "summary": summary,
211
+ "next_items": next_items,
212
+ "status": "completed",
213
+ "message": "작업 요약이 완료되었습니다.",
214
+ }
215
+
216
+
175
217
  # Export all tools
176
218
  JUPYTER_TOOLS = [
177
219
  jupyter_cell_tool,
178
220
  markdown_tool,
179
221
  ask_user_tool,
222
+ final_summary_tool,
180
223
  ]