hdsp-jupyter-extension 2.0.8__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 (88) hide show
  1. agent_server/core/notebook_generator.py +4 -4
  2. agent_server/core/rag_manager.py +12 -3
  3. agent_server/core/retriever.py +2 -1
  4. agent_server/core/vllm_embedding_service.py +8 -5
  5. agent_server/langchain/ARCHITECTURE.md +7 -51
  6. agent_server/langchain/agent.py +31 -20
  7. agent_server/langchain/custom_middleware.py +234 -31
  8. agent_server/langchain/hitl_config.py +5 -8
  9. agent_server/langchain/logging_utils.py +7 -7
  10. agent_server/langchain/prompts.py +106 -120
  11. agent_server/langchain/tools/__init__.py +1 -10
  12. agent_server/langchain/tools/file_tools.py +9 -61
  13. agent_server/langchain/tools/jupyter_tools.py +0 -1
  14. agent_server/langchain/tools/lsp_tools.py +8 -8
  15. agent_server/langchain/tools/resource_tools.py +12 -12
  16. agent_server/langchain/tools/search_tools.py +3 -158
  17. agent_server/prompts/file_action_prompts.py +8 -8
  18. agent_server/routers/langchain_agent.py +200 -125
  19. hdsp_agent_core/__init__.py +46 -47
  20. hdsp_agent_core/factory.py +6 -10
  21. hdsp_agent_core/interfaces.py +4 -2
  22. hdsp_agent_core/knowledge/__init__.py +5 -5
  23. hdsp_agent_core/knowledge/chunking.py +87 -61
  24. hdsp_agent_core/knowledge/loader.py +103 -101
  25. hdsp_agent_core/llm/service.py +192 -107
  26. hdsp_agent_core/managers/config_manager.py +16 -22
  27. hdsp_agent_core/managers/session_manager.py +5 -4
  28. hdsp_agent_core/models/__init__.py +12 -12
  29. hdsp_agent_core/models/agent.py +15 -8
  30. hdsp_agent_core/models/common.py +1 -2
  31. hdsp_agent_core/models/rag.py +48 -111
  32. hdsp_agent_core/prompts/__init__.py +12 -12
  33. hdsp_agent_core/prompts/cell_action_prompts.py +9 -7
  34. hdsp_agent_core/services/agent_service.py +10 -8
  35. hdsp_agent_core/services/chat_service.py +10 -6
  36. hdsp_agent_core/services/rag_service.py +3 -6
  37. hdsp_agent_core/tests/conftest.py +4 -1
  38. hdsp_agent_core/tests/test_factory.py +2 -2
  39. hdsp_agent_core/tests/test_services.py +12 -19
  40. {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
  41. {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
  42. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js → hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js +93 -4
  43. hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +1 -0
  44. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js → hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.58c1e128ba0b76f41f04.js +153 -130
  45. hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.58c1e128ba0b76f41f04.js.map +1 -0
  46. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js → hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.9da31d1134a53b0c4af5.js +6 -6
  47. hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.9da31d1134a53b0c4af5.js.map +1 -0
  48. {hdsp_jupyter_extension-2.0.8.dist-info → hdsp_jupyter_extension-2.0.11.dist-info}/METADATA +1 -3
  49. hdsp_jupyter_extension-2.0.11.dist-info/RECORD +144 -0
  50. jupyter_ext/__init__.py +21 -11
  51. jupyter_ext/_version.py +1 -1
  52. jupyter_ext/handlers.py +69 -50
  53. jupyter_ext/labextension/build_log.json +1 -1
  54. jupyter_ext/labextension/package.json +2 -2
  55. jupyter_ext/labextension/static/{frontend_styles_index_js.8740a527757068814573.js → frontend_styles_index_js.2d9fb488c82498c45c2d.js} +93 -4
  56. jupyter_ext/labextension/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +1 -0
  57. jupyter_ext/labextension/static/{lib_index_js.e4ff4b5779b5e049f84c.js → lib_index_js.58c1e128ba0b76f41f04.js} +153 -130
  58. jupyter_ext/labextension/static/lib_index_js.58c1e128ba0b76f41f04.js.map +1 -0
  59. jupyter_ext/labextension/static/{remoteEntry.020cdb0b864cfaa4e41e.js → remoteEntry.9da31d1134a53b0c4af5.js} +6 -6
  60. jupyter_ext/labextension/static/remoteEntry.9da31d1134a53b0c4af5.js.map +1 -0
  61. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js.map +0 -1
  62. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +0 -1
  63. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +0 -1
  64. hdsp_jupyter_extension-2.0.8.dist-info/RECORD +0 -144
  65. jupyter_ext/labextension/static/frontend_styles_index_js.8740a527757068814573.js.map +0 -1
  66. jupyter_ext/labextension/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +0 -1
  67. jupyter_ext/labextension/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +0 -1
  68. {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
  69. {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  70. {hdsp_jupyter_extension-2.0.8.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
  71. {hdsp_jupyter_extension-2.0.8.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
  72. {hdsp_jupyter_extension-2.0.8.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
  73. {hdsp_jupyter_extension-2.0.8.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
  74. {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
  75. {hdsp_jupyter_extension-2.0.8.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
  76. {hdsp_jupyter_extension-2.0.8.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
  77. {hdsp_jupyter_extension-2.0.8.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
  78. {hdsp_jupyter_extension-2.0.8.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
  79. {hdsp_jupyter_extension-2.0.8.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
  80. {hdsp_jupyter_extension-2.0.8.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
  81. {hdsp_jupyter_extension-2.0.8.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
  82. {hdsp_jupyter_extension-2.0.8.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
  83. {hdsp_jupyter_extension-2.0.8.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
  84. {hdsp_jupyter_extension-2.0.8.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
  85. {hdsp_jupyter_extension-2.0.8.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
  86. {hdsp_jupyter_extension-2.0.8.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
  87. {hdsp_jupyter_extension-2.0.8.dist-info → hdsp_jupyter_extension-2.0.11.dist-info}/WHEEL +0 -0
  88. {hdsp_jupyter_extension-2.0.8.dist-info → hdsp_jupyter_extension-2.0.11.dist-info}/licenses/LICENSE +0 -0
@@ -702,6 +702,27 @@ async def stream_agent(request: AgentRequest):
702
702
  "event": "todos",
703
703
  "data": json.dumps({"todos": todos}),
704
704
  }
705
+ # Check if all todos are completed - auto terminate
706
+ all_completed = all(
707
+ t.get("status") == "completed" for t in todos
708
+ )
709
+ if all_completed and len(todos) > 0:
710
+ logger.info(
711
+ "All %d todos completed, auto-terminating agent",
712
+ len(todos),
713
+ )
714
+ yield {
715
+ "event": "debug_clear",
716
+ "data": json.dumps({}),
717
+ }
718
+ yield {
719
+ "event": "done",
720
+ "data": json.dumps(
721
+ {"reason": "all_todos_completed"}
722
+ ),
723
+ }
724
+ return # Exit the generator
725
+
705
726
  tool_name = getattr(last_message, "name", "") or ""
706
727
  logger.info(
707
728
  "SimpleAgent ToolMessage name attribute: %s", tool_name
@@ -727,7 +748,7 @@ async def stream_agent(request: AgentRequest):
727
748
  final_answer = tool_result.get(
728
749
  "answer"
729
750
  ) or tool_result.get("parameters", {}).get("answer")
730
-
751
+
731
752
  # Check for next_items in answer field (LLM may put JSON here)
732
753
  if final_answer:
733
754
  try:
@@ -735,25 +756,41 @@ async def stream_agent(request: AgentRequest):
735
756
  if "next_items" in answer_json:
736
757
  next_items_block = f"\n\n```json\n{json.dumps(answer_json, ensure_ascii=False, indent=2)}\n```"
737
758
  # Get summary for the main text
738
- summary_text = tool_result.get(
739
- "summary"
740
- ) or tool_result.get("parameters", {}).get("summary") or ""
741
- final_answer = summary_text + next_items_block
742
- logger.info("Extracted next_items from answer field")
759
+ summary_text = (
760
+ tool_result.get("summary")
761
+ or tool_result.get(
762
+ "parameters", {}
763
+ ).get("summary")
764
+ or ""
765
+ )
766
+ final_answer = (
767
+ summary_text + next_items_block
768
+ )
769
+ logger.info(
770
+ "Extracted next_items from answer field"
771
+ )
743
772
  except (json.JSONDecodeError, TypeError):
744
773
  pass
745
-
774
+
746
775
  # Check for next_items in summary field (Gemini puts JSON here)
747
776
  summary = tool_result.get(
748
777
  "summary"
749
- ) or tool_result.get("parameters", {}).get("summary")
750
- if summary and "next_items" not in (final_answer or ""):
778
+ ) or tool_result.get("parameters", {}).get(
779
+ "summary"
780
+ )
781
+ if summary and "next_items" not in (
782
+ final_answer or ""
783
+ ):
751
784
  try:
752
785
  summary_json = json.loads(summary)
753
786
  if "next_items" in summary_json:
754
787
  next_items_block = f"\n\n```json\n{json.dumps(summary_json, ensure_ascii=False, indent=2)}\n```"
755
- final_answer = (final_answer or "") + next_items_block
756
- logger.info("Extracted next_items from summary field")
788
+ final_answer = (
789
+ final_answer or ""
790
+ ) + next_items_block
791
+ logger.info(
792
+ "Extracted next_items from summary field"
793
+ )
757
794
  except (json.JSONDecodeError, TypeError):
758
795
  pass
759
796
  if final_answer:
@@ -872,19 +909,32 @@ async def stream_agent(request: AgentRequest):
872
909
  "event": "todos",
873
910
  "data": json.dumps({"todos": todos}),
874
911
  }
912
+ # Check if all todos are completed - terminate early
913
+ all_completed = all(
914
+ t.get("status") == "completed" for t in todos
915
+ )
916
+ if all_completed and len(todos) > 0:
917
+ logger.info(
918
+ "All %d todos completed in AIMessage tool_calls, auto-terminating",
919
+ len(todos),
920
+ )
921
+ yield {
922
+ "event": "debug_clear",
923
+ "data": json.dumps({}),
924
+ }
925
+ yield {
926
+ "event": "done",
927
+ "data": json.dumps(
928
+ {"reason": "all_todos_completed"}
929
+ ),
930
+ }
931
+ return # Exit before executing more tool calls
875
932
  for tool_call in tool_calls:
876
933
  tool_name = tool_call.get("name", "unknown")
877
934
  tool_args = tool_call.get("args", {})
878
935
 
879
936
  # Create detailed status message for search tools
880
937
  if tool_name in (
881
- "search_workspace_tool",
882
- "search_workspace",
883
- ):
884
- pattern = tool_args.get("pattern", "")
885
- path = tool_args.get("path", ".")
886
- status_msg = f"🔍 검색 실행: grep/rg '{pattern}' in {path}"
887
- elif tool_name in (
888
938
  "search_notebook_cells_tool",
889
939
  "search_notebook_cells",
890
940
  ):
@@ -950,34 +1000,6 @@ async def stream_agent(request: AgentRequest):
950
1000
  }
951
1001
  ),
952
1002
  }
953
- elif tool_name in (
954
- "search_workspace_tool",
955
- "search_workspace",
956
- ):
957
- # Search workspace - emit tool_call for client-side execution
958
- produced_output = True
959
- yield {
960
- "event": "tool_call",
961
- "data": json.dumps(
962
- {
963
- "tool": "search_workspace",
964
- "pattern": tool_args.get(
965
- "pattern", ""
966
- ),
967
- "file_types": tool_args.get(
968
- "file_types",
969
- ["*.py", "*.ipynb"],
970
- ),
971
- "path": tool_args.get("path", "."),
972
- "max_results": tool_args.get(
973
- "max_results", 50
974
- ),
975
- "case_sensitive": tool_args.get(
976
- "case_sensitive", False
977
- ),
978
- }
979
- ),
980
- }
981
1003
  elif tool_name in (
982
1004
  "search_notebook_cells_tool",
983
1005
  "search_notebook_cells",
@@ -1030,16 +1052,31 @@ async def stream_agent(request: AgentRequest):
1030
1052
  content = "\n".join(text_parts)
1031
1053
 
1032
1054
  # Filter out raw JSON tool responses
1055
+ content_stripped = content.strip() if content else ""
1056
+
1057
+ # Filter out tool call JSON (but allow summary/next_items JSON for frontend rendering)
1058
+ is_json_tool_response = (
1059
+ content_stripped.startswith('{"tool":')
1060
+ or content_stripped.startswith('{ "tool":')
1061
+ or content_stripped.startswith('{"tool" :')
1062
+ or content_stripped.startswith('{"status":')
1063
+ or '"pending_execution"' in content
1064
+ or '"status": "complete"' in content
1065
+ or (
1066
+ '"tool"' in content
1067
+ and '"write_todos"' in content
1068
+ )
1069
+ or (
1070
+ '"tool"' in content
1071
+ and '"arguments"' in content
1072
+ and content_stripped.startswith("{")
1073
+ )
1074
+ )
1033
1075
  if (
1034
1076
  content
1035
1077
  and isinstance(content, str)
1036
1078
  and not has_final_answer_tool
1037
- and not (
1038
- content.strip().startswith('{"tool":')
1039
- or content.strip().startswith('{"status":')
1040
- or '"pending_execution"' in content
1041
- or '"status": "complete"' in content
1042
- )
1079
+ and not is_json_tool_response
1043
1080
  ):
1044
1081
  # Check if we've already emitted this content (prevents duplicates)
1045
1082
  content_hash = hash(content)
@@ -1161,7 +1198,7 @@ async def stream_agent(request: AgentRequest):
1161
1198
  content=(
1162
1199
  "You MUST respond with a valid tool call. "
1163
1200
  "Available tools: jupyter_cell_tool (for Python code), markdown_tool (for text), "
1164
- "list_files_tool (to list files), read_file_tool (to read files). "
1201
+ "execute_command_tool (to search files with find/grep), read_file_tool (to read files). "
1165
1202
  "Choose the most appropriate tool and provide valid JSON arguments."
1166
1203
  )
1167
1204
  ),
@@ -1280,11 +1317,7 @@ async def stream_agent(request: AgentRequest):
1280
1317
  }
1281
1318
  ),
1282
1319
  }
1283
- elif tool_name in (
1284
- "read_file_tool",
1285
- "list_files_tool",
1286
- "search_workspace_tool",
1287
- ):
1320
+ elif tool_name == "read_file_tool":
1288
1321
  # For file operations, generate code with the LLM
1289
1322
  logger.info(
1290
1323
  "Fallback: Generating code for %s via LLM",
@@ -1447,11 +1480,13 @@ async def resume_agent(request: ResumeRequest):
1447
1480
  )
1448
1481
  yield {
1449
1482
  "event": "error",
1450
- "data": json.dumps({
1451
- "error": "Session expired or not found",
1452
- "code": "CHECKPOINT_NOT_FOUND",
1453
- "message": "이전 세션을 찾을 수 없습니다. 서버가 재시작되었거나 세션이 만료되었습니다. 새로운 대화를 시작해주세요.",
1454
- }),
1483
+ "data": json.dumps(
1484
+ {
1485
+ "error": "Session expired or not found",
1486
+ "code": "CHECKPOINT_NOT_FOUND",
1487
+ "message": "이전 세션을 찾을 수 없습니다. 서버가 재시작되었거나 세션이 만료되었습니다. 새로운 대화를 시작해주세요.",
1488
+ }
1489
+ ),
1455
1490
  }
1456
1491
  return
1457
1492
 
@@ -1471,7 +1506,9 @@ async def resume_agent(request: ResumeRequest):
1471
1506
  len(_simple_agent_instances),
1472
1507
  )
1473
1508
  else:
1474
- logger.info("Resume: Creating new agent for key %s", agent_cache_key[:8])
1509
+ logger.info(
1510
+ "Resume: Creating new agent for key %s", agent_cache_key[:8]
1511
+ )
1475
1512
  agent = create_simple_chat_agent(
1476
1513
  llm_config=config_dict,
1477
1514
  workspace_root=resolved_workspace_root,
@@ -1654,6 +1691,27 @@ async def resume_agent(request: ResumeRequest):
1654
1691
  "event": "todos",
1655
1692
  "data": json.dumps({"todos": todos}),
1656
1693
  }
1694
+ # Check if all todos are completed - auto terminate
1695
+ all_completed = all(
1696
+ t.get("status") == "completed" for t in todos
1697
+ )
1698
+ if all_completed and len(todos) > 0:
1699
+ logger.info(
1700
+ "Resume: All %d todos completed, auto-terminating agent",
1701
+ len(todos),
1702
+ )
1703
+ yield {
1704
+ "event": "debug_clear",
1705
+ "data": json.dumps({}),
1706
+ }
1707
+ yield {
1708
+ "event": "done",
1709
+ "data": json.dumps(
1710
+ {"reason": "all_todos_completed"}
1711
+ ),
1712
+ }
1713
+ return # Exit the generator
1714
+
1657
1715
  tool_name = getattr(last_message, "name", "") or ""
1658
1716
  logger.info(
1659
1717
  "Resume ToolMessage name attribute: %s", tool_name
@@ -1677,7 +1735,7 @@ async def resume_agent(request: ResumeRequest):
1677
1735
  final_answer = tool_result.get(
1678
1736
  "answer"
1679
1737
  ) or tool_result.get("parameters", {}).get("answer")
1680
-
1738
+
1681
1739
  # Check for next_items in answer field (LLM may put JSON here)
1682
1740
  if final_answer:
1683
1741
  try:
@@ -1685,25 +1743,41 @@ async def resume_agent(request: ResumeRequest):
1685
1743
  if "next_items" in answer_json:
1686
1744
  next_items_block = f"\n\n```json\n{json.dumps(answer_json, ensure_ascii=False, indent=2)}\n```"
1687
1745
  # Get summary for the main text
1688
- summary_text = tool_result.get(
1689
- "summary"
1690
- ) or tool_result.get("parameters", {}).get("summary") or ""
1691
- final_answer = summary_text + next_items_block
1692
- logger.info("Resume: Extracted next_items from answer field")
1746
+ summary_text = (
1747
+ tool_result.get("summary")
1748
+ or tool_result.get(
1749
+ "parameters", {}
1750
+ ).get("summary")
1751
+ or ""
1752
+ )
1753
+ final_answer = (
1754
+ summary_text + next_items_block
1755
+ )
1756
+ logger.info(
1757
+ "Resume: Extracted next_items from answer field"
1758
+ )
1693
1759
  except (json.JSONDecodeError, TypeError):
1694
1760
  pass
1695
-
1761
+
1696
1762
  # Check for next_items in summary field (Gemini puts JSON here)
1697
1763
  summary = tool_result.get(
1698
1764
  "summary"
1699
- ) or tool_result.get("parameters", {}).get("summary")
1700
- if summary and "next_items" not in (final_answer or ""):
1765
+ ) or tool_result.get("parameters", {}).get(
1766
+ "summary"
1767
+ )
1768
+ if summary and "next_items" not in (
1769
+ final_answer or ""
1770
+ ):
1701
1771
  try:
1702
1772
  summary_json = json.loads(summary)
1703
1773
  if "next_items" in summary_json:
1704
1774
  next_items_block = f"\n\n```json\n{json.dumps(summary_json, ensure_ascii=False, indent=2)}\n```"
1705
- final_answer = (final_answer or "") + next_items_block
1706
- logger.info("Resume: Extracted next_items from summary field")
1775
+ final_answer = (
1776
+ final_answer or ""
1777
+ ) + next_items_block
1778
+ logger.info(
1779
+ "Resume: Extracted next_items from summary field"
1780
+ )
1707
1781
  except (json.JSONDecodeError, TypeError):
1708
1782
  pass
1709
1783
  if final_answer:
@@ -1775,16 +1849,27 @@ async def resume_agent(request: ResumeRequest):
1775
1849
  content = "\n".join(text_parts)
1776
1850
 
1777
1851
  # Filter out raw JSON tool responses
1852
+ content_stripped = content.strip() if content else ""
1853
+ # Filter out tool call JSON (but allow summary/next_items JSON for frontend rendering)
1854
+ is_json_tool_response = (
1855
+ content_stripped.startswith('{"tool":')
1856
+ or content_stripped.startswith('{ "tool":')
1857
+ or content_stripped.startswith('{"tool" :')
1858
+ or content_stripped.startswith('{"status":')
1859
+ or '"pending_execution"' in content
1860
+ or '"status": "complete"' in content
1861
+ or ('"tool"' in content and '"write_todos"' in content)
1862
+ or (
1863
+ '"tool"' in content
1864
+ and '"arguments"' in content
1865
+ and content_stripped.startswith("{")
1866
+ )
1867
+ )
1778
1868
  if (
1779
1869
  content
1780
1870
  and isinstance(content, str)
1781
1871
  and not has_final_answer_tool
1782
- and not (
1783
- content.strip().startswith('{"tool":')
1784
- or content.strip().startswith('{"status":')
1785
- or '"pending_execution"' in content
1786
- or '"status": "complete"' in content
1787
- )
1872
+ and not is_json_tool_response
1788
1873
  ):
1789
1874
  # Check if we've already emitted this content (prevents duplicates)
1790
1875
  content_hash = hash(content)
@@ -1835,6 +1920,26 @@ async def resume_agent(request: ResumeRequest):
1835
1920
  "event": "todos",
1836
1921
  "data": json.dumps({"todos": todos}),
1837
1922
  }
1923
+ # Check if all todos are completed - terminate early
1924
+ all_completed = all(
1925
+ t.get("status") == "completed" for t in todos
1926
+ )
1927
+ if all_completed and len(todos) > 0:
1928
+ logger.info(
1929
+ "Resume: All %d todos completed in AIMessage tool_calls, auto-terminating",
1930
+ len(todos),
1931
+ )
1932
+ yield {
1933
+ "event": "debug_clear",
1934
+ "data": json.dumps({}),
1935
+ }
1936
+ yield {
1937
+ "event": "done",
1938
+ "data": json.dumps(
1939
+ {"reason": "all_todos_completed"}
1940
+ ),
1941
+ }
1942
+ return # Exit before executing more tool calls
1838
1943
 
1839
1944
  # Process tool calls
1840
1945
  for tool_call in new_tool_calls:
@@ -1850,13 +1955,6 @@ async def resume_agent(request: ResumeRequest):
1850
1955
 
1851
1956
  # Create detailed status message for search tools
1852
1957
  if tool_name in (
1853
- "search_workspace_tool",
1854
- "search_workspace",
1855
- ):
1856
- pattern = tool_args.get("pattern", "")
1857
- path = tool_args.get("path", ".")
1858
- status_msg = f"🔍 검색 실행: grep/rg '{pattern}' in {path}"
1859
- elif tool_name in (
1860
1958
  "search_notebook_cells_tool",
1861
1959
  "search_notebook_cells",
1862
1960
  ):
@@ -1914,33 +2012,6 @@ async def resume_agent(request: ResumeRequest):
1914
2012
  }
1915
2013
  ),
1916
2014
  }
1917
- elif tool_name in (
1918
- "search_workspace_tool",
1919
- "search_workspace",
1920
- ):
1921
- # Search workspace - emit tool_call for client-side execution
1922
- yield {
1923
- "event": "tool_call",
1924
- "data": json.dumps(
1925
- {
1926
- "tool": "search_workspace",
1927
- "pattern": tool_args.get(
1928
- "pattern", ""
1929
- ),
1930
- "file_types": tool_args.get(
1931
- "file_types",
1932
- ["*.py", "*.ipynb"],
1933
- ),
1934
- "path": tool_args.get("path", "."),
1935
- "max_results": tool_args.get(
1936
- "max_results", 50
1937
- ),
1938
- "case_sensitive": tool_args.get(
1939
- "case_sensitive", False
1940
- ),
1941
- }
1942
- ),
1943
- }
1944
2015
  elif tool_name in (
1945
2016
  "search_notebook_cells_tool",
1946
2017
  "search_notebook_cells",
@@ -2057,20 +2128,24 @@ async def resume_agent(request: ResumeRequest):
2057
2128
  )
2058
2129
  yield {
2059
2130
  "event": "error",
2060
- "data": json.dumps({
2061
- "error": "Session state lost",
2062
- "code": "CONTENTS_NOT_SPECIFIED",
2063
- "error_type": type(e).__name__,
2064
- "message": "세션 상태가 손실되었습니다. 서버가 재시작되었거나 세션이 만료되었습니다. 새로운 대화를 시작해주세요.",
2065
- }),
2131
+ "data": json.dumps(
2132
+ {
2133
+ "error": "Session state lost",
2134
+ "code": "CONTENTS_NOT_SPECIFIED",
2135
+ "error_type": type(e).__name__,
2136
+ "message": "세션 상태가 손실되었습니다. 서버가 재시작되었거나 세션이 만료되었습니다. 새로운 대화를 시작해주세요.",
2137
+ }
2138
+ ),
2066
2139
  }
2067
2140
  else:
2068
2141
  yield {
2069
2142
  "event": "error",
2070
- "data": json.dumps({
2071
- "error": error_msg,
2072
- "error_type": type(e).__name__,
2073
- }),
2143
+ "data": json.dumps(
2144
+ {
2145
+ "error": error_msg,
2146
+ "error_type": type(e).__name__,
2147
+ }
2148
+ ),
2074
2149
  }
2075
2150
 
2076
2151
  return EventSourceResponse(event_generator())
@@ -15,77 +15,76 @@ Modules:
15
15
  __version__ = "1.0.0"
16
16
 
17
17
  # Models
18
+ # Knowledge
19
+ from hdsp_agent_core.knowledge import (
20
+ LIBRARY_DESCRIPTIONS,
21
+ DocumentChunker,
22
+ KnowledgeBase,
23
+ KnowledgeLoader,
24
+ LibraryDetector,
25
+ chunk_file,
26
+ get_knowledge_base,
27
+ get_knowledge_loader,
28
+ get_library_detector,
29
+ )
30
+
31
+ # LLM
32
+ from hdsp_agent_core.llm import (
33
+ LLMService,
34
+ call_llm,
35
+ call_llm_stream,
36
+ )
37
+
38
+ # Managers
39
+ from hdsp_agent_core.managers import (
40
+ ConfigManager,
41
+ SessionManager,
42
+ get_config_manager,
43
+ get_session_manager,
44
+ )
18
45
  from hdsp_agent_core.models import (
19
46
  # Common
20
47
  APIResponse,
48
+ # Chat
49
+ ChatRequest,
50
+ ChatResponse,
51
+ # RAG
52
+ ChunkingConfig,
53
+ EmbeddingConfig,
21
54
  ErrorInfo,
55
+ # Agent
56
+ ExecutionPlan,
22
57
  GeminiConfig,
58
+ IndexStatusResponse,
23
59
  LLMConfig,
24
60
  NotebookContext,
25
61
  OpenAIConfig,
26
- ToolCall,
27
- VLLMConfig,
28
- # Agent
29
- ExecutionPlan,
30
62
  PlanRequest,
31
63
  PlanResponse,
32
64
  PlanStep,
33
- RefineRequest,
34
- RefineResponse,
35
- ReplanRequest,
36
- ReplanResponse,
37
- ValidationIssue,
38
- # Chat
39
- ChatRequest,
40
- ChatResponse,
41
- StreamChunk,
42
- # RAG
43
- ChunkingConfig,
44
- EmbeddingConfig,
45
- IndexStatusResponse,
46
65
  QdrantConfig,
47
66
  RAGConfig,
67
+ RefineRequest,
68
+ RefineResponse,
48
69
  ReindexRequest,
49
70
  ReindexResponse,
71
+ ReplanRequest,
72
+ ReplanResponse,
50
73
  SearchRequest,
51
74
  SearchResponse,
75
+ StreamChunk,
76
+ ToolCall,
77
+ ValidationIssue,
78
+ VLLMConfig,
52
79
  WatchdogConfig,
53
80
  )
54
81
 
55
- # Managers
56
- from hdsp_agent_core.managers import (
57
- ConfigManager,
58
- SessionManager,
59
- get_config_manager,
60
- get_session_manager,
61
- )
62
-
63
- # LLM
64
- from hdsp_agent_core.llm import (
65
- LLMService,
66
- call_llm,
67
- call_llm_stream,
68
- )
69
-
70
- # Knowledge
71
- from hdsp_agent_core.knowledge import (
72
- DocumentChunker,
73
- KnowledgeBase,
74
- KnowledgeLoader,
75
- LibraryDetector,
76
- chunk_file,
77
- get_knowledge_base,
78
- get_knowledge_loader,
79
- get_library_detector,
80
- LIBRARY_DESCRIPTIONS,
81
- )
82
-
83
82
  # Prompts
84
83
  from hdsp_agent_core.prompts import (
85
- PLAN_GENERATION_PROMPT,
84
+ ADAPTIVE_REPLAN_PROMPT,
86
85
  CODE_GENERATION_PROMPT,
87
86
  ERROR_REFINEMENT_PROMPT,
88
- ADAPTIVE_REPLAN_PROMPT,
87
+ PLAN_GENERATION_PROMPT,
89
88
  format_plan_prompt,
90
89
  format_refine_prompt,
91
90
  format_replan_prompt,
@@ -21,8 +21,9 @@ logger = logging.getLogger(__name__)
21
21
 
22
22
  class AgentMode(Enum):
23
23
  """Agent execution mode"""
24
+
24
25
  EMBEDDED = "embedded" # Direct in-process execution
25
- PROXY = "proxy" # HTTP proxy to external server
26
+ PROXY = "proxy" # HTTP proxy to external server
26
27
 
27
28
 
28
29
  class ServiceFactory:
@@ -84,9 +85,7 @@ class ServiceFactory:
84
85
  elif mode_str == "proxy":
85
86
  return AgentMode.PROXY
86
87
  else:
87
- logger.warning(
88
- f"Unknown HDSP_AGENT_MODE '{mode_str}', defaulting to proxy"
89
- )
88
+ logger.warning(f"Unknown HDSP_AGENT_MODE '{mode_str}', defaulting to proxy")
90
89
  return AgentMode.PROXY
91
90
 
92
91
  @property
@@ -164,16 +163,13 @@ class ServiceFactory:
164
163
 
165
164
  # Create proxy service instances
166
165
  self._agent_service = ProxyAgentService(
167
- base_url=self._server_url,
168
- timeout=self._timeout
166
+ base_url=self._server_url, timeout=self._timeout
169
167
  )
170
168
  self._chat_service = ProxyChatService(
171
- base_url=self._server_url,
172
- timeout=self._timeout
169
+ base_url=self._server_url, timeout=self._timeout
173
170
  )
174
171
  self._rag_service = ProxyRAGService(
175
- base_url=self._server_url,
176
- timeout=self._timeout
172
+ base_url=self._server_url, timeout=self._timeout
177
173
  )
178
174
 
179
175
  # Optionally validate connectivity