hdsp-jupyter-extension 2.0.6__py3-none-any.whl → 2.0.7__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/reflection_engine.py +0 -1
- agent_server/knowledge/watchdog_service.py +1 -1
- agent_server/langchain/ARCHITECTURE.md +1193 -0
- agent_server/langchain/agent.py +74 -588
- agent_server/langchain/custom_middleware.py +636 -0
- agent_server/langchain/executors/__init__.py +2 -7
- agent_server/langchain/executors/notebook_searcher.py +46 -38
- agent_server/langchain/hitl_config.py +66 -0
- agent_server/langchain/llm_factory.py +166 -0
- agent_server/langchain/logging_utils.py +184 -0
- agent_server/langchain/prompts.py +119 -0
- agent_server/langchain/state.py +16 -6
- agent_server/langchain/tools/__init__.py +6 -0
- agent_server/langchain/tools/file_tools.py +91 -129
- agent_server/langchain/tools/jupyter_tools.py +18 -18
- agent_server/langchain/tools/resource_tools.py +161 -0
- agent_server/langchain/tools/search_tools.py +198 -216
- agent_server/langchain/tools/shell_tools.py +54 -0
- agent_server/main.py +4 -1
- agent_server/routers/health.py +1 -1
- agent_server/routers/langchain_agent.py +940 -285
- hdsp_agent_core/prompts/auto_agent_prompts.py +3 -3
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.02d346171474a0fb2dc1.js → hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js +312 -6
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +1 -0
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js → hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js +1547 -330
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js.map +1 -0
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.addf2fa038fa60304aa2.js → hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js +8 -8
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js.map +1 -0
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js → hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js +209 -2
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js → hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js +2 -209
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +1 -0
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js → hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js +3 -212
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +1 -0
- {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.7.dist-info}/METADATA +2 -1
- {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.7.dist-info}/RECORD +71 -68
- jupyter_ext/_version.py +1 -1
- jupyter_ext/handlers.py +1176 -58
- jupyter_ext/labextension/build_log.json +1 -1
- jupyter_ext/labextension/package.json +2 -2
- jupyter_ext/labextension/static/{frontend_styles_index_js.02d346171474a0fb2dc1.js → frontend_styles_index_js.4770ec0fb2d173b6deb4.js} +312 -6
- jupyter_ext/labextension/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +1 -0
- jupyter_ext/labextension/static/{lib_index_js.a223ea20056954479ae9.js → lib_index_js.29cf4312af19e86f82af.js} +1547 -330
- jupyter_ext/labextension/static/lib_index_js.29cf4312af19e86f82af.js.map +1 -0
- jupyter_ext/labextension/static/{remoteEntry.addf2fa038fa60304aa2.js → remoteEntry.61343eb4cf0577e74b50.js} +8 -8
- jupyter_ext/labextension/static/remoteEntry.61343eb4cf0577e74b50.js.map +1 -0
- jupyter_ext/labextension/static/{vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js → vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js} +209 -2
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +1 -0
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js +2 -209
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +1 -0
- jupyter_ext/labextension/static/{vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js → vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js} +3 -212
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +1 -0
- jupyter_ext/resource_usage.py +180 -0
- jupyter_ext/tests/test_handlers.py +58 -0
- agent_server/langchain/executors/jupyter_executor.py +0 -429
- agent_server/langchain/middleware/__init__.py +0 -36
- agent_server/langchain/middleware/code_search_middleware.py +0 -278
- agent_server/langchain/middleware/error_handling_middleware.py +0 -338
- agent_server/langchain/middleware/jupyter_execution_middleware.py +0 -301
- agent_server/langchain/middleware/rag_middleware.py +0 -227
- agent_server/langchain/middleware/validation_middleware.py +0 -240
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.02d346171474a0fb2dc1.js.map +0 -1
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.addf2fa038fa60304aa2.js.map +0 -1
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +0 -1
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +0 -1
- hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -1
- jupyter_ext/labextension/static/frontend_styles_index_js.02d346171474a0fb2dc1.js.map +0 -1
- jupyter_ext/labextension/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
- jupyter_ext/labextension/static/remoteEntry.addf2fa038fa60304aa2.js.map +0 -1
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +0 -1
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +0 -1
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -1
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.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.6.data → hdsp_jupyter_extension-2.0.7.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.6.data → hdsp_jupyter_extension-2.0.7.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.6.data → hdsp_jupyter_extension-2.0.7.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.6.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.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.6.data → hdsp_jupyter_extension-2.0.7.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.6.data → hdsp_jupyter_extension-2.0.7.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.6.data → hdsp_jupyter_extension-2.0.7.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.6.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
- {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.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.6.dist-info → hdsp_jupyter_extension-2.0.7.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -205,30 +205,42 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
205
205
|
// Todo list state (from TodoListMiddleware)
|
|
206
206
|
const [todos, setTodos] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
|
|
207
207
|
const [isTodoExpanded, setIsTodoExpanded] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
208
|
+
// Agent thread ID for context persistence across cycles (SummarizationMiddleware support)
|
|
209
|
+
const [agentThreadId, setAgentThreadId] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
|
|
210
|
+
// Rejection feedback mode - when user clicks reject, wait for optional feedback
|
|
211
|
+
const [isRejectionMode, setIsRejectionMode] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
|
212
|
+
const [pendingRejectionInterrupt, setPendingRejectionInterrupt] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
|
|
208
213
|
const interruptMessageIdRef = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)(null);
|
|
209
214
|
const approvalPendingRef = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)(false);
|
|
210
215
|
const pendingToolCallsRef = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)([]);
|
|
211
216
|
const handledToolCallKeysRef = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)(new Set());
|
|
217
|
+
const isNotebookWidget = (widget) => {
|
|
218
|
+
if (!widget)
|
|
219
|
+
return false;
|
|
220
|
+
if (widget instanceof _jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_2__.NotebookPanel)
|
|
221
|
+
return true;
|
|
222
|
+
const model = widget?.content?.model;
|
|
223
|
+
return Boolean(model && (model.cells || model.sharedModel?.cells));
|
|
224
|
+
};
|
|
212
225
|
const getActiveNotebookPanel = () => {
|
|
213
226
|
const app = window.jupyterapp;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return currentWidget;
|
|
218
|
-
}
|
|
219
|
-
if ('content' in currentWidget && currentWidget.content?.model) {
|
|
220
|
-
return currentWidget;
|
|
221
|
-
}
|
|
227
|
+
const currentWidget = app?.shell?.currentWidget;
|
|
228
|
+
if (isNotebookWidget(currentWidget)) {
|
|
229
|
+
return currentWidget;
|
|
222
230
|
}
|
|
223
|
-
|
|
231
|
+
if (notebookTracker?.currentWidget && isNotebookWidget(notebookTracker.currentWidget)) {
|
|
232
|
+
return notebookTracker.currentWidget;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
224
235
|
};
|
|
225
236
|
const insertCell = (notebook, cellType, source) => {
|
|
226
237
|
const model = notebook.content?.model;
|
|
227
|
-
|
|
238
|
+
const cellCount = model?.cells?.length ?? model?.sharedModel?.cells?.length;
|
|
239
|
+
if (!model?.sharedModel || cellCount === undefined) {
|
|
228
240
|
console.warn('[AgentPanel] Notebook model not ready for insert');
|
|
229
241
|
return null;
|
|
230
242
|
}
|
|
231
|
-
const insertIndex =
|
|
243
|
+
const insertIndex = cellCount;
|
|
232
244
|
model.sharedModel.insertCell(insertIndex, {
|
|
233
245
|
cell_type: cellType,
|
|
234
246
|
source
|
|
@@ -236,6 +248,21 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
236
248
|
notebook.content.activeCellIndex = insertIndex;
|
|
237
249
|
return insertIndex;
|
|
238
250
|
};
|
|
251
|
+
const deleteCell = (notebook, cellIndex) => {
|
|
252
|
+
const model = notebook.content?.model;
|
|
253
|
+
const cellCount = model?.cells?.length ?? model?.sharedModel?.cells?.length;
|
|
254
|
+
if (!model?.sharedModel || cellCount === undefined) {
|
|
255
|
+
console.warn('[AgentPanel] Notebook model not ready for delete');
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
if (cellIndex < 0 || cellIndex >= cellCount) {
|
|
259
|
+
console.warn('[AgentPanel] Invalid cell index for delete:', cellIndex);
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
model.sharedModel.deleteCell(cellIndex);
|
|
263
|
+
console.log('[AgentPanel] Deleted rejected cell at index:', cellIndex);
|
|
264
|
+
return true;
|
|
265
|
+
};
|
|
239
266
|
const executeCell = async (notebook, cellIndex) => {
|
|
240
267
|
try {
|
|
241
268
|
await notebook.sessionContext.ready;
|
|
@@ -347,6 +374,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
347
374
|
approvalPendingRef.current = pendingToolCallsRef.current.length > 0;
|
|
348
375
|
return next;
|
|
349
376
|
};
|
|
377
|
+
const getAutoApproveEnabled = (config) => (Boolean(config?.autoApprove));
|
|
350
378
|
const queueApprovalCell = (code) => {
|
|
351
379
|
const notebook = getActiveNotebookPanel();
|
|
352
380
|
if (!notebook) {
|
|
@@ -750,6 +778,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
750
778
|
}
|
|
751
779
|
};
|
|
752
780
|
// Python 에러 메시지 처리 및 파일 수정 요청
|
|
781
|
+
// Returns true if handled, false if should fall back to regular chat
|
|
753
782
|
const handlePythonErrorFix = async (errorMessage) => {
|
|
754
783
|
console.log('[AgentPanel] Handling Python error fix request');
|
|
755
784
|
// 1. 에러가 발생한 파일 경로 추출
|
|
@@ -761,8 +790,8 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
761
790
|
console.log('[AgentPanel] All files:', allFilePaths);
|
|
762
791
|
if (!errorFilePath && !mainFilePath) {
|
|
763
792
|
// 파일 경로를 찾을 수 없으면 일반 채팅으로 처리
|
|
764
|
-
console.log('[AgentPanel] No file path found,
|
|
765
|
-
return;
|
|
793
|
+
console.log('[AgentPanel] No file path found, falling back to regular chat stream');
|
|
794
|
+
return false;
|
|
766
795
|
}
|
|
767
796
|
// 2. 주요 파일 내용 로드
|
|
768
797
|
const targetFile = errorFilePath || mainFilePath;
|
|
@@ -783,7 +812,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
783
812
|
console.error('[AgentPanel] File fix API error:', error);
|
|
784
813
|
addErrorMessage('파일 수정 요청 실패: ' + error.message);
|
|
785
814
|
}
|
|
786
|
-
return;
|
|
815
|
+
return true; // Handled (even if failed)
|
|
787
816
|
}
|
|
788
817
|
// 3. 관련 파일들 (imports) 로드
|
|
789
818
|
const localImports = extractLocalImports(mainContent);
|
|
@@ -820,10 +849,12 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
820
849
|
setIsLoading(true);
|
|
821
850
|
const result = await apiService.fileAction(request);
|
|
822
851
|
handleFileFixResponse(result.response, result.fixedFiles);
|
|
852
|
+
return true; // Successfully handled
|
|
823
853
|
}
|
|
824
854
|
catch (error) {
|
|
825
855
|
console.error('[AgentPanel] File fix API error:', error);
|
|
826
856
|
addErrorMessage('파일 수정 요청 실패: ' + error.message);
|
|
857
|
+
return true; // Handled (even if failed)
|
|
827
858
|
}
|
|
828
859
|
finally {
|
|
829
860
|
setIsLoading(false);
|
|
@@ -972,6 +1003,203 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
972
1003
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
973
1004
|
}
|
|
974
1005
|
}, [messages, isStreaming]);
|
|
1006
|
+
const handleNextItemSelection = async (nextText) => {
|
|
1007
|
+
const trimmed = nextText.trim();
|
|
1008
|
+
if (!trimmed)
|
|
1009
|
+
return;
|
|
1010
|
+
// Check if there's a pending interrupt (HITL approval waiting)
|
|
1011
|
+
const hasPendingInterrupt = interruptData !== null;
|
|
1012
|
+
// Check if all todos are completed
|
|
1013
|
+
const allTodosCompleted = todos.length === 0 || todos.every(t => t.status === 'completed');
|
|
1014
|
+
// Block if there's a pending interrupt OR if agent is running with incomplete todos
|
|
1015
|
+
// Allow if all todos are completed even if streaming is finishing up
|
|
1016
|
+
if (hasPendingInterrupt || (isAgentRunning && !allTodosCompleted)) {
|
|
1017
|
+
showNotification('다른 작업이 진행 중입니다. 완료 후 다시 시도해주세요.', 'warning');
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
await sendChatMessage({
|
|
1021
|
+
displayContent: trimmed,
|
|
1022
|
+
clearInput: true
|
|
1023
|
+
});
|
|
1024
|
+
};
|
|
1025
|
+
const sendChatMessage = async ({ displayContent, llmPrompt, textarea, clearInput = true }) => {
|
|
1026
|
+
const displayContentText = displayContent || (llmPrompt ? '셀 분석 요청' : '');
|
|
1027
|
+
if (!displayContentText && !llmPrompt)
|
|
1028
|
+
return;
|
|
1029
|
+
// Check if API key is configured before sending
|
|
1030
|
+
let currentConfig = llmConfig;
|
|
1031
|
+
if (!currentConfig) {
|
|
1032
|
+
// Config not loaded yet, try to load from localStorage
|
|
1033
|
+
currentConfig = (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getLLMConfig)() || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getDefaultLLMConfig)();
|
|
1034
|
+
setLlmConfig(currentConfig);
|
|
1035
|
+
}
|
|
1036
|
+
// Check API key using ApiKeyManager
|
|
1037
|
+
const hasApiKey = (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.hasValidApiKey)(currentConfig);
|
|
1038
|
+
const autoApproveEnabled = getAutoApproveEnabled(currentConfig);
|
|
1039
|
+
if (!hasApiKey) {
|
|
1040
|
+
// Show error message and open settings
|
|
1041
|
+
const providerName = currentConfig?.provider || 'LLM';
|
|
1042
|
+
const errorMessage = {
|
|
1043
|
+
id: makeMessageId(),
|
|
1044
|
+
role: 'assistant',
|
|
1045
|
+
content: `API Key가 설정되지 않았습니다.\n\n${providerName === 'gemini' ? 'Gemini' : providerName === 'openai' ? 'OpenAI' : 'vLLM'} API Key를 먼저 설정해주세요.\n\n설정 버튼을 클릭하여 API Key를 입력하세요.`,
|
|
1046
|
+
timestamp: Date.now()
|
|
1047
|
+
};
|
|
1048
|
+
setMessages(prev => [...prev, errorMessage]);
|
|
1049
|
+
setShowSettings(true);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const userMessage = {
|
|
1053
|
+
id: makeMessageId(),
|
|
1054
|
+
role: 'user',
|
|
1055
|
+
content: displayContentText,
|
|
1056
|
+
timestamp: Date.now()
|
|
1057
|
+
};
|
|
1058
|
+
setMessages(prev => [...prev, userMessage]);
|
|
1059
|
+
if (clearInput && displayContentText) {
|
|
1060
|
+
setInput('');
|
|
1061
|
+
}
|
|
1062
|
+
setIsLoading(true);
|
|
1063
|
+
setIsStreaming(true);
|
|
1064
|
+
// Clear todos only when starting a new task (all completed or no todos)
|
|
1065
|
+
// Keep todos if there are pending/in_progress items (continuation of current task)
|
|
1066
|
+
const hasActiveTodos = todos.some(t => t.status === 'pending' || t.status === 'in_progress');
|
|
1067
|
+
if (!hasActiveTodos) {
|
|
1068
|
+
setTodos([]);
|
|
1069
|
+
}
|
|
1070
|
+
setDebugStatus(null);
|
|
1071
|
+
// Clear the data attribute and ref after using it
|
|
1072
|
+
if (textarea && llmPrompt) {
|
|
1073
|
+
textarea.removeAttribute('data-llm-prompt');
|
|
1074
|
+
pendingLlmPromptRef.current = null;
|
|
1075
|
+
}
|
|
1076
|
+
// Create assistant message ID for streaming updates
|
|
1077
|
+
const assistantMessageId = makeMessageId('assistant');
|
|
1078
|
+
let streamedContent = '';
|
|
1079
|
+
setStreamingMessageId(assistantMessageId);
|
|
1080
|
+
// Add empty assistant message that will be updated during streaming
|
|
1081
|
+
const initialAssistantMessage = {
|
|
1082
|
+
id: assistantMessageId,
|
|
1083
|
+
role: 'assistant',
|
|
1084
|
+
content: '',
|
|
1085
|
+
timestamp: Date.now()
|
|
1086
|
+
};
|
|
1087
|
+
setMessages(prev => [...prev, initialAssistantMessage]);
|
|
1088
|
+
try {
|
|
1089
|
+
// Use LLM prompt if available, otherwise use the display content
|
|
1090
|
+
const messageToSend = llmPrompt || displayContentText;
|
|
1091
|
+
console.log('[AgentPanel] Sending message with agentThreadId:', agentThreadId);
|
|
1092
|
+
await apiService.sendMessageStream({
|
|
1093
|
+
message: messageToSend,
|
|
1094
|
+
conversationId: conversationId || undefined,
|
|
1095
|
+
llmConfig: currentConfig // Include API keys with request
|
|
1096
|
+
},
|
|
1097
|
+
// onChunk callback - update message content incrementally
|
|
1098
|
+
(chunk) => {
|
|
1099
|
+
streamedContent += chunk;
|
|
1100
|
+
setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
|
|
1101
|
+
? { ...msg, content: streamedContent }
|
|
1102
|
+
: msg));
|
|
1103
|
+
},
|
|
1104
|
+
// onMetadata callback - update conversationId and metadata
|
|
1105
|
+
(metadata) => {
|
|
1106
|
+
if (metadata.conversationId && !conversationId) {
|
|
1107
|
+
setConversationId(metadata.conversationId);
|
|
1108
|
+
}
|
|
1109
|
+
if (metadata.provider || metadata.model) {
|
|
1110
|
+
setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
|
|
1111
|
+
? {
|
|
1112
|
+
...msg,
|
|
1113
|
+
metadata: {
|
|
1114
|
+
...msg.metadata,
|
|
1115
|
+
provider: metadata.provider,
|
|
1116
|
+
model: metadata.model
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
: msg));
|
|
1120
|
+
}
|
|
1121
|
+
},
|
|
1122
|
+
// onDebug callback - show debug status in gray
|
|
1123
|
+
(status) => {
|
|
1124
|
+
setDebugStatus(status);
|
|
1125
|
+
},
|
|
1126
|
+
// onInterrupt callback - show approval dialog
|
|
1127
|
+
(interrupt) => {
|
|
1128
|
+
approvalPendingRef.current = true;
|
|
1129
|
+
// Capture threadId from interrupt for context persistence
|
|
1130
|
+
if (interrupt.threadId && !agentThreadId) {
|
|
1131
|
+
setAgentThreadId(interrupt.threadId);
|
|
1132
|
+
console.log('[AgentPanel] Captured agentThreadId from interrupt:', interrupt.threadId);
|
|
1133
|
+
}
|
|
1134
|
+
// Auto-approve search/file/resource tools - execute immediately without user interaction
|
|
1135
|
+
if (interrupt.action === 'search_workspace_tool'
|
|
1136
|
+
|| interrupt.action === 'search_notebook_cells_tool'
|
|
1137
|
+
|| interrupt.action === 'check_resource_tool'
|
|
1138
|
+
|| interrupt.action === 'list_files_tool'
|
|
1139
|
+
|| interrupt.action === 'read_file_tool') {
|
|
1140
|
+
void handleAutoToolInterrupt(interrupt);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (autoApproveEnabled) {
|
|
1144
|
+
void resumeFromInterrupt(interrupt, 'approve');
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (interrupt.action === 'jupyter_cell_tool' && interrupt.args?.code) {
|
|
1148
|
+
const shouldQueue = shouldExecuteInNotebook(interrupt.args.code);
|
|
1149
|
+
if (isAutoApprovedCode(interrupt.args.code)) {
|
|
1150
|
+
if (shouldQueue) {
|
|
1151
|
+
queueApprovalCell(interrupt.args.code);
|
|
1152
|
+
}
|
|
1153
|
+
void resumeFromInterrupt(interrupt, 'approve');
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (shouldQueue) {
|
|
1157
|
+
queueApprovalCell(interrupt.args.code);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
setInterruptData(interrupt);
|
|
1161
|
+
upsertInterruptMessage(interrupt);
|
|
1162
|
+
setIsLoading(false);
|
|
1163
|
+
setIsStreaming(false);
|
|
1164
|
+
},
|
|
1165
|
+
// onTodos callback - update todo list UI
|
|
1166
|
+
(newTodos) => {
|
|
1167
|
+
setTodos(newTodos);
|
|
1168
|
+
},
|
|
1169
|
+
// onDebugClear callback - clear debug status
|
|
1170
|
+
() => {
|
|
1171
|
+
setDebugStatus(null);
|
|
1172
|
+
},
|
|
1173
|
+
// onToolCall callback - add cells to notebook
|
|
1174
|
+
handleToolCall,
|
|
1175
|
+
// onComplete callback - capture thread_id for context persistence
|
|
1176
|
+
(data) => {
|
|
1177
|
+
if (data.threadId) {
|
|
1178
|
+
setAgentThreadId(data.threadId);
|
|
1179
|
+
console.log('[AgentPanel] Captured agentThreadId for context persistence:', data.threadId);
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
// threadId - pass existing thread_id to continue context
|
|
1183
|
+
agentThreadId || undefined);
|
|
1184
|
+
}
|
|
1185
|
+
catch (error) {
|
|
1186
|
+
const message = error instanceof Error ? error.message : 'Failed to send message';
|
|
1187
|
+
setDebugStatus(`오류: ${message}`);
|
|
1188
|
+
// Update the assistant message with error
|
|
1189
|
+
setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
|
|
1190
|
+
? {
|
|
1191
|
+
...msg,
|
|
1192
|
+
content: streamedContent + `\n\nError: ${message}`
|
|
1193
|
+
}
|
|
1194
|
+
: msg));
|
|
1195
|
+
}
|
|
1196
|
+
finally {
|
|
1197
|
+
setIsLoading(false);
|
|
1198
|
+
setIsStreaming(false);
|
|
1199
|
+
setStreamingMessageId(null);
|
|
1200
|
+
// Keep completed todos visible after the run
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
975
1203
|
// Extract and store code blocks from messages, setup button listeners
|
|
976
1204
|
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
|
|
977
1205
|
// Use a small delay to ensure DOM is updated after message rendering
|
|
@@ -1014,6 +1242,40 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
1014
1242
|
// Use event delegation - attach single listener to container
|
|
1015
1243
|
const handleContainerClick = async (e) => {
|
|
1016
1244
|
const target = e.target;
|
|
1245
|
+
const nextItem = target.closest('.jp-next-items-item');
|
|
1246
|
+
if (nextItem) {
|
|
1247
|
+
e.stopPropagation();
|
|
1248
|
+
e.preventDefault();
|
|
1249
|
+
const subject = nextItem.querySelector('.jp-next-items-subject')?.textContent?.trim() || '';
|
|
1250
|
+
const description = nextItem.querySelector('.jp-next-items-description')?.textContent?.trim() || '';
|
|
1251
|
+
const nextText = subject && description
|
|
1252
|
+
? `${subject}\n\n${description}`
|
|
1253
|
+
: (subject || description);
|
|
1254
|
+
if (nextText) {
|
|
1255
|
+
void handleNextItemSelection(nextText);
|
|
1256
|
+
}
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
// Handle expand/collapse button
|
|
1260
|
+
if (target.classList.contains('code-block-toggle') || target.closest('.code-block-toggle')) {
|
|
1261
|
+
const button = target.classList.contains('code-block-toggle')
|
|
1262
|
+
? target
|
|
1263
|
+
: target.closest('.code-block-toggle');
|
|
1264
|
+
e.stopPropagation();
|
|
1265
|
+
e.preventDefault();
|
|
1266
|
+
const container = button.closest('.code-block-container');
|
|
1267
|
+
if (!container)
|
|
1268
|
+
return;
|
|
1269
|
+
const isExpanded = container.classList.toggle('is-expanded');
|
|
1270
|
+
button.setAttribute('aria-expanded', String(isExpanded));
|
|
1271
|
+
button.setAttribute('title', isExpanded ? '접기' : '전체 보기');
|
|
1272
|
+
button.setAttribute('aria-label', isExpanded ? '접기' : '전체 보기');
|
|
1273
|
+
const icon = button.querySelector('.code-block-toggle-icon');
|
|
1274
|
+
if (icon) {
|
|
1275
|
+
icon.textContent = isExpanded ? '▴' : '▾';
|
|
1276
|
+
}
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1017
1279
|
// Handle copy button
|
|
1018
1280
|
if (target.classList.contains('code-block-copy') || target.closest('.code-block-copy')) {
|
|
1019
1281
|
const button = target.classList.contains('code-block-copy')
|
|
@@ -1570,6 +1832,12 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
1570
1832
|
if (lines.length === 0) {
|
|
1571
1833
|
return true;
|
|
1572
1834
|
}
|
|
1835
|
+
if (lines.some(line => (line.startsWith('!')
|
|
1836
|
+
|| line.startsWith('%%bash')
|
|
1837
|
+
|| line.startsWith('%%sh')
|
|
1838
|
+
|| line.startsWith('%%shell')))) {
|
|
1839
|
+
return false;
|
|
1840
|
+
}
|
|
1573
1841
|
const disallowedPatterns = [
|
|
1574
1842
|
/(^|[^=!<>])=([^=]|$)/,
|
|
1575
1843
|
/\.read_[a-zA-Z0-9_]*\s*\(/,
|
|
@@ -1591,6 +1859,160 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
1591
1859
|
];
|
|
1592
1860
|
return lines.every(line => allowedPatterns.some(pattern => pattern.test(line)));
|
|
1593
1861
|
};
|
|
1862
|
+
const userRequestedNotebookExecution = () => {
|
|
1863
|
+
const lastUserMessage = [...messages]
|
|
1864
|
+
.reverse()
|
|
1865
|
+
.find((msg) => isChatMessage(msg) && msg.role === 'user');
|
|
1866
|
+
const content = lastUserMessage?.content ?? '';
|
|
1867
|
+
return /노트북|셀|cell|notebook|jupyter/i.test(content);
|
|
1868
|
+
};
|
|
1869
|
+
const isShellCell = (code) => {
|
|
1870
|
+
const lines = code.split('\n');
|
|
1871
|
+
const firstLine = lines.find(line => line.trim().length > 0)?.trim() || '';
|
|
1872
|
+
if (firstLine.startsWith('%%bash')
|
|
1873
|
+
|| firstLine.startsWith('%%sh')
|
|
1874
|
+
|| firstLine.startsWith('%%shell')) {
|
|
1875
|
+
return true;
|
|
1876
|
+
}
|
|
1877
|
+
return lines.some(line => line.trim().startsWith('!'));
|
|
1878
|
+
};
|
|
1879
|
+
const extractShellCommand = (code) => {
|
|
1880
|
+
const lines = code.split('\n');
|
|
1881
|
+
const firstNonEmptyIndex = lines.findIndex(line => line.trim().length > 0);
|
|
1882
|
+
if (firstNonEmptyIndex === -1) {
|
|
1883
|
+
return '';
|
|
1884
|
+
}
|
|
1885
|
+
const firstLine = lines[firstNonEmptyIndex].trim();
|
|
1886
|
+
if (firstLine.startsWith('%%bash')
|
|
1887
|
+
|| firstLine.startsWith('%%sh')
|
|
1888
|
+
|| firstLine.startsWith('%%shell')) {
|
|
1889
|
+
const script = lines.slice(firstNonEmptyIndex + 1).join('\n').trim();
|
|
1890
|
+
if (!script) {
|
|
1891
|
+
return '';
|
|
1892
|
+
}
|
|
1893
|
+
const escaped = script
|
|
1894
|
+
.replace(/\\/g, '\\\\')
|
|
1895
|
+
.replace(/'/g, "\\'")
|
|
1896
|
+
.replace(/\r?\n/g, '\\n');
|
|
1897
|
+
return `bash -lc $'${escaped}'`;
|
|
1898
|
+
}
|
|
1899
|
+
const shellLines = lines
|
|
1900
|
+
.map(line => line.trim())
|
|
1901
|
+
.filter(line => line.startsWith('!'))
|
|
1902
|
+
.map(line => line.replace(/^!+/, '').trim())
|
|
1903
|
+
.filter(Boolean);
|
|
1904
|
+
return shellLines.join('\n');
|
|
1905
|
+
};
|
|
1906
|
+
const buildPythonCommand = (code) => (`python3 -c ${JSON.stringify(code)}`);
|
|
1907
|
+
const shouldExecuteInNotebook = (code) => {
|
|
1908
|
+
const notebook = getActiveNotebookPanel();
|
|
1909
|
+
if (!notebook) {
|
|
1910
|
+
return false;
|
|
1911
|
+
}
|
|
1912
|
+
if (isShellCell(code) && !userRequestedNotebookExecution()) {
|
|
1913
|
+
return false;
|
|
1914
|
+
}
|
|
1915
|
+
return true;
|
|
1916
|
+
};
|
|
1917
|
+
const truncateOutputLines = (output, maxLines = 2) => {
|
|
1918
|
+
const lines = output.split(/\r?\n/).filter(line => line.length > 0);
|
|
1919
|
+
const text = lines.slice(0, maxLines).join('\n');
|
|
1920
|
+
return { text, truncated: lines.length > maxLines };
|
|
1921
|
+
};
|
|
1922
|
+
const createCommandOutputMessage = (command) => {
|
|
1923
|
+
const messageId = makeMessageId('command-output');
|
|
1924
|
+
const outputMessage = {
|
|
1925
|
+
id: messageId,
|
|
1926
|
+
role: 'system',
|
|
1927
|
+
content: `🐚 ${command}\n`,
|
|
1928
|
+
timestamp: Date.now(),
|
|
1929
|
+
metadata: {
|
|
1930
|
+
kind: 'shell-output',
|
|
1931
|
+
command
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
setMessages(prev => [...prev, outputMessage]);
|
|
1935
|
+
return messageId;
|
|
1936
|
+
};
|
|
1937
|
+
const appendCommandOutputMessage = (messageId, text, stream) => {
|
|
1938
|
+
if (!text)
|
|
1939
|
+
return;
|
|
1940
|
+
const prefix = stream === 'stderr' ? '[stderr] ' : '';
|
|
1941
|
+
setMessages(prev => prev.map(msg => {
|
|
1942
|
+
if (msg.id !== messageId || !('role' in msg)) {
|
|
1943
|
+
return msg;
|
|
1944
|
+
}
|
|
1945
|
+
const chatMsg = msg;
|
|
1946
|
+
return {
|
|
1947
|
+
...chatMsg,
|
|
1948
|
+
content: `${chatMsg.content}${prefix}${text}`
|
|
1949
|
+
};
|
|
1950
|
+
}));
|
|
1951
|
+
};
|
|
1952
|
+
const executeSubprocessCommand = async (command, timeout, onOutput, stdin) => {
|
|
1953
|
+
try {
|
|
1954
|
+
const result = await apiService.executeCommandStream(command, { timeout, onOutput, stdin });
|
|
1955
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout : '';
|
|
1956
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : '';
|
|
1957
|
+
const combined = [stdout, stderr].filter(Boolean).join('\n');
|
|
1958
|
+
const summary = truncateOutputLines(combined, 2);
|
|
1959
|
+
const truncated = summary.truncated || Boolean(result.truncated);
|
|
1960
|
+
const output = summary.text || '(no output)';
|
|
1961
|
+
if (result.success) {
|
|
1962
|
+
return {
|
|
1963
|
+
success: true,
|
|
1964
|
+
output,
|
|
1965
|
+
returncode: result.returncode ?? null,
|
|
1966
|
+
command,
|
|
1967
|
+
truncated
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
const errorText = summary.text || result.error || stderr || 'Command failed';
|
|
1971
|
+
return {
|
|
1972
|
+
success: false,
|
|
1973
|
+
output,
|
|
1974
|
+
error: errorText,
|
|
1975
|
+
returncode: result.returncode ?? null,
|
|
1976
|
+
command,
|
|
1977
|
+
truncated
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
catch (error) {
|
|
1981
|
+
const message = error instanceof Error ? error.message : 'Command execution failed';
|
|
1982
|
+
if (onOutput) {
|
|
1983
|
+
onOutput({ stream: 'stderr', text: `${message}\n` });
|
|
1984
|
+
}
|
|
1985
|
+
const summary = truncateOutputLines(message, 2);
|
|
1986
|
+
return {
|
|
1987
|
+
success: false,
|
|
1988
|
+
output: '',
|
|
1989
|
+
error: summary.text || 'Command execution failed',
|
|
1990
|
+
returncode: null,
|
|
1991
|
+
command,
|
|
1992
|
+
truncated: summary.truncated
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
const executeCodeViaSubprocess = async (code, timeout) => {
|
|
1997
|
+
const isShell = isShellCell(code);
|
|
1998
|
+
const command = isShell ? extractShellCommand(code) : buildPythonCommand(code);
|
|
1999
|
+
if (!command) {
|
|
2000
|
+
return {
|
|
2001
|
+
success: false,
|
|
2002
|
+
output: '',
|
|
2003
|
+
error: isShell ? 'Shell command is empty' : 'Python command is empty',
|
|
2004
|
+
returncode: null,
|
|
2005
|
+
command,
|
|
2006
|
+
truncated: false,
|
|
2007
|
+
execution_method: 'subprocess'
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
const result = await executeSubprocessCommand(command, timeout);
|
|
2011
|
+
return {
|
|
2012
|
+
...result,
|
|
2013
|
+
execution_method: 'subprocess'
|
|
2014
|
+
};
|
|
2015
|
+
};
|
|
1594
2016
|
const upsertInterruptMessage = (interrupt) => {
|
|
1595
2017
|
const interruptMessageId = interruptMessageIdRef.current || makeMessageId('interrupt');
|
|
1596
2018
|
interruptMessageIdRef.current = interruptMessageId;
|
|
@@ -1609,7 +2031,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
1609
2031
|
return [...prev, interruptMessage];
|
|
1610
2032
|
});
|
|
1611
2033
|
};
|
|
1612
|
-
const clearInterruptMessage = () => {
|
|
2034
|
+
const clearInterruptMessage = (decision) => {
|
|
1613
2035
|
if (!interruptMessageIdRef.current)
|
|
1614
2036
|
return;
|
|
1615
2037
|
const messageId = interruptMessageIdRef.current;
|
|
@@ -1624,7 +2046,8 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
1624
2046
|
...chatMsg.metadata,
|
|
1625
2047
|
interrupt: {
|
|
1626
2048
|
...(chatMsg.metadata?.interrupt || {}),
|
|
1627
|
-
resolved: true
|
|
2049
|
+
resolved: true,
|
|
2050
|
+
decision: decision || 'approve' // Track approve/reject decision
|
|
1628
2051
|
}
|
|
1629
2052
|
}
|
|
1630
2053
|
};
|
|
@@ -1632,38 +2055,454 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
1632
2055
|
return msg;
|
|
1633
2056
|
}));
|
|
1634
2057
|
};
|
|
1635
|
-
const
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
clearInterruptMessage();
|
|
1640
|
-
let resumeDecision = decision;
|
|
1641
|
-
let resumeArgs = undefined;
|
|
1642
|
-
if (decision === 'approve') {
|
|
1643
|
-
const executed = await executePendingApproval();
|
|
1644
|
-
if (executed && executed.tool === 'jupyter_cell') {
|
|
1645
|
-
resumeDecision = 'edit';
|
|
1646
|
-
resumeArgs = {
|
|
1647
|
-
code: executed.code,
|
|
1648
|
-
execution_result: executed.execution_result
|
|
1649
|
-
};
|
|
1650
|
-
}
|
|
1651
|
-
else if (executed && executed.tool === 'markdown') {
|
|
1652
|
-
resumeDecision = 'edit';
|
|
1653
|
-
resumeArgs = {
|
|
1654
|
-
content: executed.content
|
|
1655
|
-
};
|
|
1656
|
-
}
|
|
2058
|
+
const validateRelativePath = (path) => {
|
|
2059
|
+
const trimmed = path.trim();
|
|
2060
|
+
if (trimmed.startsWith('/') || trimmed.startsWith('\\') || /^[A-Za-z]:/.test(trimmed)) {
|
|
2061
|
+
return { valid: false, error: 'Absolute paths are not allowed' };
|
|
1657
2062
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
approvalPendingRef.current = pendingToolCallsRef.current.length > 0;
|
|
2063
|
+
if (trimmed.includes('..')) {
|
|
2064
|
+
return { valid: false, error: 'Path traversal (..) is not allowed' };
|
|
1661
2065
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
2066
|
+
return { valid: true };
|
|
2067
|
+
};
|
|
2068
|
+
const normalizeContentsPath = (path) => {
|
|
2069
|
+
const trimmed = path.trim();
|
|
2070
|
+
if (!trimmed || trimmed === '.' || trimmed === './') {
|
|
2071
|
+
return '';
|
|
2072
|
+
}
|
|
2073
|
+
return trimmed
|
|
2074
|
+
.replace(/^\.\/+/, '')
|
|
2075
|
+
.replace(/^\/+/, '')
|
|
2076
|
+
.replace(/\/+$/, '');
|
|
2077
|
+
};
|
|
2078
|
+
const globToRegex = (pattern) => {
|
|
2079
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
2080
|
+
const regex = `^${escaped.replace(/\*/g, '.*').replace(/\?/g, '.')}$`;
|
|
2081
|
+
return new RegExp(regex);
|
|
2082
|
+
};
|
|
2083
|
+
const fetchContentsModel = async (path, options) => {
|
|
2084
|
+
try {
|
|
2085
|
+
const { PageConfig, URLExt } = await Promise.resolve(/*! import() */).then(__webpack_require__.t.bind(__webpack_require__, /*! @jupyterlab/coreutils */ "webpack/sharing/consume/default/@jupyterlab/coreutils", 23));
|
|
2086
|
+
const baseUrl = PageConfig.getBaseUrl();
|
|
2087
|
+
const normalizedPath = normalizeContentsPath(path);
|
|
2088
|
+
const apiUrl = URLExt.join(baseUrl, 'api/contents', normalizedPath);
|
|
2089
|
+
const query = new URLSearchParams();
|
|
2090
|
+
if (options?.content !== undefined) {
|
|
2091
|
+
query.set('content', options.content ? '1' : '0');
|
|
2092
|
+
}
|
|
2093
|
+
if (options?.format) {
|
|
2094
|
+
query.set('format', options.format);
|
|
2095
|
+
}
|
|
2096
|
+
const url = query.toString() ? `${apiUrl}?${query.toString()}` : apiUrl;
|
|
2097
|
+
const response = await fetch(url, {
|
|
2098
|
+
method: 'GET',
|
|
2099
|
+
credentials: 'include',
|
|
2100
|
+
});
|
|
2101
|
+
if (!response.ok) {
|
|
2102
|
+
return { success: false, error: `Failed to load contents: ${response.status}` };
|
|
2103
|
+
}
|
|
2104
|
+
const data = await response.json();
|
|
2105
|
+
return { success: true, data };
|
|
2106
|
+
}
|
|
2107
|
+
catch (error) {
|
|
2108
|
+
const message = error instanceof Error ? error.message : 'Failed to load contents';
|
|
2109
|
+
return { success: false, error: message };
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
const executeListFilesTool = async (params) => {
|
|
2113
|
+
const pathCheck = validateRelativePath(params.path);
|
|
2114
|
+
if (!pathCheck.valid) {
|
|
2115
|
+
return { success: false, error: pathCheck.error };
|
|
2116
|
+
}
|
|
2117
|
+
const pattern = (params.pattern || '*').trim() || '*';
|
|
2118
|
+
const recursive = params.recursive ?? false;
|
|
2119
|
+
const matcher = globToRegex(pattern);
|
|
2120
|
+
const maxEntries = 500;
|
|
2121
|
+
const files = [];
|
|
2122
|
+
const pendingDirs = [normalizeContentsPath(params.path)];
|
|
2123
|
+
const visited = new Set();
|
|
2124
|
+
while (pendingDirs.length > 0 && files.length < maxEntries) {
|
|
2125
|
+
const dirPath = pendingDirs.shift() ?? '';
|
|
2126
|
+
if (visited.has(dirPath)) {
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
visited.add(dirPath);
|
|
2130
|
+
const contentsResult = await fetchContentsModel(dirPath, { content: true });
|
|
2131
|
+
if (!contentsResult.success) {
|
|
2132
|
+
return { success: false, error: contentsResult.error };
|
|
2133
|
+
}
|
|
2134
|
+
const model = contentsResult.data;
|
|
2135
|
+
if (!model || model.type !== 'directory' || !Array.isArray(model.content)) {
|
|
2136
|
+
const displayPath = dirPath || '.';
|
|
2137
|
+
return { success: false, error: `Not a directory: ${displayPath}` };
|
|
2138
|
+
}
|
|
2139
|
+
for (const entry of model.content) {
|
|
2140
|
+
if (!entry) {
|
|
2141
|
+
continue;
|
|
2142
|
+
}
|
|
2143
|
+
const name = entry.name || entry.path?.split('/').pop() || '';
|
|
2144
|
+
const entryPath = entry.path || name;
|
|
2145
|
+
const isDir = entry.type === 'directory';
|
|
2146
|
+
if (matcher.test(name)) {
|
|
2147
|
+
files.push({
|
|
2148
|
+
path: entryPath,
|
|
2149
|
+
isDir,
|
|
2150
|
+
size: isDir ? 0 : (entry.size ?? 0),
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
if (recursive && isDir && entryPath) {
|
|
2154
|
+
pendingDirs.push(entryPath);
|
|
2155
|
+
}
|
|
2156
|
+
if (files.length >= maxEntries) {
|
|
2157
|
+
break;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
const formatted = files.map((file) => {
|
|
2162
|
+
const icon = file.isDir ? '📁' : '📄';
|
|
2163
|
+
const sizeInfo = file.isDir ? '' : ` (${file.size} bytes)`;
|
|
2164
|
+
return `${icon} ${file.path}${file.isDir ? '/' : sizeInfo}`;
|
|
2165
|
+
}).join('\n');
|
|
2166
|
+
return {
|
|
2167
|
+
success: true,
|
|
2168
|
+
output: formatted || '(empty directory)',
|
|
2169
|
+
metadata: { count: files.length, files }
|
|
2170
|
+
};
|
|
2171
|
+
};
|
|
2172
|
+
const executeReadFileTool = async (params) => {
|
|
2173
|
+
if (!params.path) {
|
|
2174
|
+
return { success: false, error: 'Path is required' };
|
|
2175
|
+
}
|
|
2176
|
+
const pathCheck = validateRelativePath(params.path);
|
|
2177
|
+
if (!pathCheck.valid) {
|
|
2178
|
+
return { success: false, error: pathCheck.error };
|
|
2179
|
+
}
|
|
2180
|
+
const maxLines = typeof params.maxLines === 'number' ? params.maxLines : 1000;
|
|
2181
|
+
const safeMaxLines = Math.max(0, maxLines);
|
|
2182
|
+
const contentsResult = await fetchContentsModel(params.path, { content: true, format: 'text' });
|
|
2183
|
+
if (!contentsResult.success) {
|
|
2184
|
+
return { success: false, error: contentsResult.error };
|
|
2185
|
+
}
|
|
2186
|
+
const model = contentsResult.data;
|
|
2187
|
+
if (!model) {
|
|
2188
|
+
return { success: false, error: 'File not found' };
|
|
2189
|
+
}
|
|
2190
|
+
if (model.type === 'directory') {
|
|
2191
|
+
return { success: false, error: `Path is a directory: ${params.path}` };
|
|
2192
|
+
}
|
|
2193
|
+
let content = model.content ?? '';
|
|
2194
|
+
if (model.format === 'base64') {
|
|
2195
|
+
return { success: false, error: 'Binary file content is not supported' };
|
|
2196
|
+
}
|
|
2197
|
+
if (typeof content !== 'string') {
|
|
2198
|
+
content = JSON.stringify(content, null, 2);
|
|
2199
|
+
}
|
|
2200
|
+
const lines = content.split('\n');
|
|
2201
|
+
const sliced = lines.slice(0, safeMaxLines);
|
|
2202
|
+
return {
|
|
2203
|
+
success: true,
|
|
2204
|
+
output: sliced.join('\n'),
|
|
2205
|
+
metadata: {
|
|
2206
|
+
lineCount: sliced.length,
|
|
2207
|
+
truncated: lines.length > safeMaxLines
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2210
|
+
};
|
|
2211
|
+
// Helper function for auto-approving search/file tools with execution results
|
|
2212
|
+
const handleAutoToolInterrupt = async (interrupt) => {
|
|
2213
|
+
const { threadId, action, args } = interrupt;
|
|
2214
|
+
console.log('[AgentPanel] Auto-approving tool:', action, args);
|
|
2215
|
+
try {
|
|
2216
|
+
let executionResult;
|
|
2217
|
+
if (action === 'search_workspace_tool') {
|
|
2218
|
+
setDebugStatus(`🔍 검색 실행 중: ${args?.pattern || ''}`);
|
|
2219
|
+
executionResult = await apiService.searchWorkspace({
|
|
2220
|
+
pattern: args?.pattern || '',
|
|
2221
|
+
file_types: args?.file_types || ['*.py', '*.ipynb'],
|
|
2222
|
+
path: args?.path || '.',
|
|
2223
|
+
max_results: args?.max_results || 50,
|
|
2224
|
+
case_sensitive: args?.case_sensitive || false
|
|
2225
|
+
});
|
|
2226
|
+
console.log('[AgentPanel] search_workspace result:', executionResult);
|
|
2227
|
+
}
|
|
2228
|
+
else if (action === 'search_notebook_cells_tool') {
|
|
2229
|
+
setDebugStatus(`🔍 노트북 검색 실행 중: ${args?.pattern || ''}`);
|
|
2230
|
+
executionResult = await apiService.searchNotebookCells({
|
|
2231
|
+
pattern: args?.pattern || '',
|
|
2232
|
+
notebook_path: args?.notebook_path,
|
|
2233
|
+
cell_type: args?.cell_type,
|
|
2234
|
+
max_results: args?.max_results || 30,
|
|
2235
|
+
case_sensitive: args?.case_sensitive || false
|
|
2236
|
+
});
|
|
2237
|
+
console.log('[AgentPanel] search_notebook_cells result:', executionResult);
|
|
2238
|
+
}
|
|
2239
|
+
else if (action === 'check_resource_tool') {
|
|
2240
|
+
const filesList = Array.isArray(args?.files) ? args.files : (args?.files ? [args.files] : []);
|
|
2241
|
+
setDebugStatus(`📊 리소스 체크 중: ${filesList.join(', ') || 'system'}`);
|
|
2242
|
+
executionResult = await apiService.checkResource({
|
|
2243
|
+
files: filesList,
|
|
2244
|
+
dataframes: args?.dataframes || [],
|
|
2245
|
+
file_size_command: args?.file_size_command || '',
|
|
2246
|
+
dataframe_check_code: args?.dataframe_check_code || ''
|
|
2247
|
+
});
|
|
2248
|
+
console.log('[AgentPanel] check_resource result:', executionResult);
|
|
2249
|
+
}
|
|
2250
|
+
else if (action === 'list_files_tool') {
|
|
2251
|
+
setDebugStatus('📂 파일 목록 조회 중...');
|
|
2252
|
+
const listParams = {
|
|
2253
|
+
path: typeof args?.path === 'string' ? args.path : '.',
|
|
2254
|
+
recursive: args?.recursive ?? false,
|
|
2255
|
+
pattern: args?.pattern ?? undefined
|
|
2256
|
+
};
|
|
2257
|
+
executionResult = await executeListFilesTool(listParams);
|
|
2258
|
+
console.log('[AgentPanel] list_files result:', executionResult);
|
|
2259
|
+
}
|
|
2260
|
+
else if (action === 'read_file_tool') {
|
|
2261
|
+
setDebugStatus('📄 파일 읽는 중...');
|
|
2262
|
+
const readParams = {
|
|
2263
|
+
path: typeof args?.path === 'string' ? args.path : '',
|
|
2264
|
+
encoding: typeof args?.encoding === 'string' ? args.encoding : undefined,
|
|
2265
|
+
maxLines: args?.max_lines ?? args?.maxLines
|
|
2266
|
+
};
|
|
2267
|
+
executionResult = await executeReadFileTool(readParams);
|
|
2268
|
+
console.log('[AgentPanel] read_file result:', executionResult);
|
|
2269
|
+
}
|
|
2270
|
+
else {
|
|
2271
|
+
console.warn('[AgentPanel] Unknown auto tool:', action);
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
// Resume with execution result
|
|
2275
|
+
const resumeArgs = {
|
|
2276
|
+
...args,
|
|
2277
|
+
execution_result: executionResult
|
|
2278
|
+
};
|
|
2279
|
+
// Clear interrupt state (don't show approval UI for search)
|
|
2280
|
+
setInterruptData(null);
|
|
2281
|
+
// Create new assistant message for continued response
|
|
2282
|
+
const assistantMessageId = makeMessageId('assistant');
|
|
2283
|
+
setStreamingMessageId(assistantMessageId);
|
|
2284
|
+
setMessages(prev => [
|
|
2285
|
+
...prev,
|
|
2286
|
+
{
|
|
2287
|
+
id: assistantMessageId,
|
|
2288
|
+
role: 'assistant',
|
|
2289
|
+
content: '',
|
|
2290
|
+
timestamp: Date.now()
|
|
2291
|
+
}
|
|
2292
|
+
]);
|
|
2293
|
+
let streamedContent = '';
|
|
2294
|
+
let interrupted = false;
|
|
2295
|
+
setDebugStatus('🤔 LLM 응답 대기 중');
|
|
2296
|
+
await apiService.resumeAgent(threadId, 'edit', // Use 'edit' to pass execution_result in args
|
|
2297
|
+
resumeArgs, undefined, llmConfig || undefined, (chunk) => {
|
|
2298
|
+
streamedContent += chunk;
|
|
2299
|
+
setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
|
|
2300
|
+
? { ...msg, content: streamedContent }
|
|
2301
|
+
: msg));
|
|
2302
|
+
}, (status) => {
|
|
2303
|
+
setDebugStatus(status);
|
|
2304
|
+
}, (nextInterrupt) => {
|
|
2305
|
+
interrupted = true;
|
|
2306
|
+
approvalPendingRef.current = true;
|
|
2307
|
+
const autoApproveEnabled = getAutoApproveEnabled(llmConfig || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getLLMConfig)() || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getDefaultLLMConfig)());
|
|
2308
|
+
// Handle next interrupt (could be another search or code execution)
|
|
2309
|
+
if (nextInterrupt.action === 'search_workspace_tool'
|
|
2310
|
+
|| nextInterrupt.action === 'search_notebook_cells_tool'
|
|
2311
|
+
|| nextInterrupt.action === 'check_resource_tool'
|
|
2312
|
+
|| nextInterrupt.action === 'list_files_tool'
|
|
2313
|
+
|| nextInterrupt.action === 'read_file_tool') {
|
|
2314
|
+
void handleAutoToolInterrupt(nextInterrupt);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
if (autoApproveEnabled) {
|
|
2318
|
+
void resumeFromInterrupt(nextInterrupt, 'approve');
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
if (nextInterrupt.action === 'jupyter_cell_tool' && nextInterrupt.args?.code) {
|
|
2322
|
+
const shouldQueue = shouldExecuteInNotebook(nextInterrupt.args.code);
|
|
2323
|
+
if (isAutoApprovedCode(nextInterrupt.args.code)) {
|
|
2324
|
+
if (shouldQueue) {
|
|
2325
|
+
queueApprovalCell(nextInterrupt.args.code);
|
|
2326
|
+
}
|
|
2327
|
+
void resumeFromInterrupt(nextInterrupt, 'approve');
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
if (shouldQueue) {
|
|
2331
|
+
queueApprovalCell(nextInterrupt.args.code);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
setInterruptData(nextInterrupt);
|
|
2335
|
+
upsertInterruptMessage(nextInterrupt);
|
|
2336
|
+
setIsLoading(false);
|
|
2337
|
+
setIsStreaming(false);
|
|
2338
|
+
}, (newTodos) => {
|
|
2339
|
+
setTodos(newTodos);
|
|
2340
|
+
}, () => {
|
|
2341
|
+
setDebugStatus(null);
|
|
2342
|
+
}, handleToolCall);
|
|
2343
|
+
if (!interrupted) {
|
|
2344
|
+
setIsLoading(false);
|
|
2345
|
+
setIsStreaming(false);
|
|
2346
|
+
setStreamingMessageId(null);
|
|
2347
|
+
approvalPendingRef.current = false;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
catch (error) {
|
|
2351
|
+
const message = error instanceof Error ? error.message : 'Tool execution failed';
|
|
2352
|
+
console.error('[AgentPanel] Auto tool error:', error);
|
|
2353
|
+
setDebugStatus(`오류: ${message}`);
|
|
2354
|
+
setIsLoading(false);
|
|
2355
|
+
setIsStreaming(false);
|
|
2356
|
+
approvalPendingRef.current = false;
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2359
|
+
const resumeFromInterrupt = async (interrupt, decision, feedback // Optional feedback message for rejection
|
|
2360
|
+
) => {
|
|
2361
|
+
const { threadId } = interrupt;
|
|
2362
|
+
setInterruptData(null);
|
|
2363
|
+
setDebugStatus(null);
|
|
2364
|
+
clearInterruptMessage(decision); // Pass decision to track approve/reject
|
|
2365
|
+
let resumeDecision = decision;
|
|
2366
|
+
let resumeArgs = undefined;
|
|
2367
|
+
if (decision === 'approve') {
|
|
2368
|
+
// Handle write_file_tool separately - execute file write on Jupyter server
|
|
2369
|
+
if (interrupt.action === 'write_file_tool') {
|
|
2370
|
+
try {
|
|
2371
|
+
setDebugStatus('📝 파일 쓰기 중...');
|
|
2372
|
+
const writeResult = await apiService.writeFile(interrupt.args?.path || '', interrupt.args?.content || '', {
|
|
2373
|
+
encoding: interrupt.args?.encoding || 'utf-8',
|
|
2374
|
+
overwrite: interrupt.args?.overwrite || false
|
|
2375
|
+
});
|
|
2376
|
+
console.log('[AgentPanel] write_file result:', writeResult);
|
|
2377
|
+
resumeDecision = 'edit';
|
|
2378
|
+
resumeArgs = {
|
|
2379
|
+
...interrupt.args,
|
|
2380
|
+
execution_result: writeResult
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
catch (error) {
|
|
2384
|
+
const message = error instanceof Error ? error.message : 'File write failed';
|
|
2385
|
+
console.error('[AgentPanel] write_file error:', error);
|
|
2386
|
+
resumeDecision = 'edit';
|
|
2387
|
+
resumeArgs = {
|
|
2388
|
+
...interrupt.args,
|
|
2389
|
+
execution_result: { success: false, error: message }
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
else if (interrupt.action === 'execute_command_tool') {
|
|
2394
|
+
const command = (interrupt.args?.command || '').trim();
|
|
2395
|
+
// Default stdin to "y\n" for interactive prompts (yes/no)
|
|
2396
|
+
const stdinInput = interrupt.args?.stdin ?? 'y\n';
|
|
2397
|
+
setDebugStatus('🐚 셸 명령 실행 중...');
|
|
2398
|
+
const outputMessageId = command ? createCommandOutputMessage(command) : null;
|
|
2399
|
+
const execResult = command
|
|
2400
|
+
? await executeSubprocessCommand(command, interrupt.args?.timeout, outputMessageId
|
|
2401
|
+
? (chunk) => appendCommandOutputMessage(outputMessageId, chunk.text, chunk.stream)
|
|
2402
|
+
: undefined, stdinInput)
|
|
2403
|
+
: {
|
|
2404
|
+
success: false,
|
|
2405
|
+
output: '',
|
|
2406
|
+
error: 'Command is required',
|
|
2407
|
+
returncode: null,
|
|
2408
|
+
command,
|
|
2409
|
+
truncated: false
|
|
2410
|
+
};
|
|
2411
|
+
resumeDecision = 'edit';
|
|
2412
|
+
resumeArgs = {
|
|
2413
|
+
...interrupt.args,
|
|
2414
|
+
execution_result: execResult
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
else if (interrupt.action === 'jupyter_cell_tool' && interrupt.args?.code) {
|
|
2418
|
+
const code = interrupt.args.code;
|
|
2419
|
+
if (!shouldExecuteInNotebook(code)) {
|
|
2420
|
+
setDebugStatus('🐚 서브프로세스로 코드 실행 중...');
|
|
2421
|
+
const execResult = await executeCodeViaSubprocess(code, interrupt.args?.timeout);
|
|
2422
|
+
resumeDecision = 'edit';
|
|
2423
|
+
resumeArgs = {
|
|
2424
|
+
code,
|
|
2425
|
+
execution_result: execResult
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
else {
|
|
2429
|
+
const executed = await executePendingApproval();
|
|
2430
|
+
if (executed && executed.tool === 'jupyter_cell') {
|
|
2431
|
+
resumeDecision = 'edit';
|
|
2432
|
+
resumeArgs = {
|
|
2433
|
+
code: executed.code,
|
|
2434
|
+
execution_result: executed.execution_result
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
else {
|
|
2438
|
+
const notebook = getActiveNotebookPanel();
|
|
2439
|
+
if (!notebook) {
|
|
2440
|
+
setDebugStatus('🐚 노트북이 없어 서브프로세스로 실행 중...');
|
|
2441
|
+
const execResult = await executeCodeViaSubprocess(code, interrupt.args?.timeout);
|
|
2442
|
+
resumeDecision = 'edit';
|
|
2443
|
+
resumeArgs = {
|
|
2444
|
+
code,
|
|
2445
|
+
execution_result: execResult
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
else {
|
|
2449
|
+
const cellIndex = insertCell(notebook, 'code', code);
|
|
2450
|
+
if (cellIndex === null) {
|
|
2451
|
+
setDebugStatus('🐚 노트북 모델이 없어 서브프로세스로 실행 중...');
|
|
2452
|
+
const execResult = await executeCodeViaSubprocess(code, interrupt.args?.timeout);
|
|
2453
|
+
resumeDecision = 'edit';
|
|
2454
|
+
resumeArgs = {
|
|
2455
|
+
code,
|
|
2456
|
+
execution_result: execResult
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
else {
|
|
2460
|
+
await executeCell(notebook, cellIndex);
|
|
2461
|
+
const execResult = captureExecutionResult(notebook, cellIndex);
|
|
2462
|
+
resumeDecision = 'edit';
|
|
2463
|
+
resumeArgs = {
|
|
2464
|
+
code,
|
|
2465
|
+
execution_result: execResult
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
else {
|
|
2473
|
+
const executed = await executePendingApproval();
|
|
2474
|
+
if (executed && executed.tool === 'jupyter_cell') {
|
|
2475
|
+
resumeDecision = 'edit';
|
|
2476
|
+
resumeArgs = {
|
|
2477
|
+
code: executed.code,
|
|
2478
|
+
execution_result: executed.execution_result
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
else if (executed && executed.tool === 'markdown') {
|
|
2482
|
+
resumeDecision = 'edit';
|
|
2483
|
+
resumeArgs = {
|
|
2484
|
+
content: executed.content
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
else {
|
|
2490
|
+
// Reject: delete the pending cell from notebook
|
|
2491
|
+
const pendingCall = pendingToolCallsRef.current[0];
|
|
2492
|
+
if (pendingCall && pendingCall.cellIndex !== undefined) {
|
|
2493
|
+
const notebook = getActiveNotebookPanel();
|
|
2494
|
+
if (notebook) {
|
|
2495
|
+
deleteCell(notebook, pendingCall.cellIndex);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
pendingToolCallsRef.current.shift();
|
|
2499
|
+
approvalPendingRef.current = pendingToolCallsRef.current.length > 0;
|
|
2500
|
+
}
|
|
2501
|
+
setIsLoading(true);
|
|
2502
|
+
setIsStreaming(true);
|
|
2503
|
+
let interrupted = false;
|
|
2504
|
+
// 항상 새 메시지 생성 - 승인 UI 아래에 append되도록
|
|
2505
|
+
const assistantMessageId = makeMessageId('assistant');
|
|
1667
2506
|
setStreamingMessageId(assistantMessageId);
|
|
1668
2507
|
// 새 메시지 추가 (맨 아래에 append)
|
|
1669
2508
|
setMessages(prev => [
|
|
@@ -1676,8 +2515,12 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
1676
2515
|
}
|
|
1677
2516
|
]);
|
|
1678
2517
|
let streamedContent = '';
|
|
2518
|
+
// Build feedback message for rejection
|
|
2519
|
+
const rejectionFeedback = resumeDecision === 'reject'
|
|
2520
|
+
? (feedback ? `사용자 피드백: ${feedback}` : 'User rejected this action')
|
|
2521
|
+
: undefined;
|
|
1679
2522
|
try {
|
|
1680
|
-
await apiService.resumeAgent(threadId, resumeDecision, resumeArgs,
|
|
2523
|
+
await apiService.resumeAgent(threadId, resumeDecision, resumeArgs, rejectionFeedback, llmConfig || undefined, (chunk) => {
|
|
1681
2524
|
streamedContent += chunk;
|
|
1682
2525
|
setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
|
|
1683
2526
|
? { ...msg, content: streamedContent }
|
|
@@ -1687,13 +2530,32 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
1687
2530
|
}, (nextInterrupt) => {
|
|
1688
2531
|
interrupted = true;
|
|
1689
2532
|
approvalPendingRef.current = true;
|
|
2533
|
+
const autoApproveEnabled = getAutoApproveEnabled(llmConfig || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getLLMConfig)() || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getDefaultLLMConfig)());
|
|
2534
|
+
// Auto-approve search/file/resource tools
|
|
2535
|
+
if (nextInterrupt.action === 'search_workspace_tool'
|
|
2536
|
+
|| nextInterrupt.action === 'search_notebook_cells_tool'
|
|
2537
|
+
|| nextInterrupt.action === 'check_resource_tool'
|
|
2538
|
+
|| nextInterrupt.action === 'list_files_tool'
|
|
2539
|
+
|| nextInterrupt.action === 'read_file_tool') {
|
|
2540
|
+
void handleAutoToolInterrupt(nextInterrupt);
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
if (autoApproveEnabled) {
|
|
2544
|
+
void resumeFromInterrupt(nextInterrupt, 'approve');
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
1690
2547
|
if (nextInterrupt.action === 'jupyter_cell_tool' && nextInterrupt.args?.code) {
|
|
2548
|
+
const shouldQueue = shouldExecuteInNotebook(nextInterrupt.args.code);
|
|
1691
2549
|
if (isAutoApprovedCode(nextInterrupt.args.code)) {
|
|
1692
|
-
|
|
2550
|
+
if (shouldQueue) {
|
|
2551
|
+
queueApprovalCell(nextInterrupt.args.code);
|
|
2552
|
+
}
|
|
1693
2553
|
void resumeFromInterrupt(nextInterrupt, 'approve');
|
|
1694
2554
|
return;
|
|
1695
2555
|
}
|
|
1696
|
-
|
|
2556
|
+
if (shouldQueue) {
|
|
2557
|
+
queueApprovalCell(nextInterrupt.args.code);
|
|
2558
|
+
}
|
|
1697
2559
|
}
|
|
1698
2560
|
setInterruptData(nextInterrupt);
|
|
1699
2561
|
upsertInterruptMessage(nextInterrupt);
|
|
@@ -1728,6 +2590,26 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
1728
2590
|
}
|
|
1729
2591
|
};
|
|
1730
2592
|
const handleSendMessage = async () => {
|
|
2593
|
+
// Handle rejection mode - resume with optional feedback
|
|
2594
|
+
if (isRejectionMode && pendingRejectionInterrupt) {
|
|
2595
|
+
const feedback = input.trim() || undefined; // Empty input means no feedback
|
|
2596
|
+
// Add user message bubble if there's feedback
|
|
2597
|
+
if (feedback) {
|
|
2598
|
+
const userMessage = {
|
|
2599
|
+
id: makeMessageId(),
|
|
2600
|
+
role: 'user',
|
|
2601
|
+
content: feedback,
|
|
2602
|
+
timestamp: Date.now(),
|
|
2603
|
+
};
|
|
2604
|
+
setMessages(prev => [...prev, userMessage]);
|
|
2605
|
+
}
|
|
2606
|
+
setInput('');
|
|
2607
|
+
setIsRejectionMode(false);
|
|
2608
|
+
const interruptToResume = pendingRejectionInterrupt;
|
|
2609
|
+
setPendingRejectionInterrupt(null);
|
|
2610
|
+
await resumeFromInterrupt(interruptToResume, 'reject', feedback);
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
1731
2613
|
// Check if there's an LLM prompt stored (from cell action)
|
|
1732
2614
|
const textarea = messagesEndRef.current?.parentElement?.querySelector('.jp-agent-input');
|
|
1733
2615
|
const llmPrompt = pendingLlmPromptRef.current || textarea?.getAttribute('data-llm-prompt');
|
|
@@ -1753,19 +2635,23 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
1753
2635
|
if (!notebook) {
|
|
1754
2636
|
// Python 에러가 감지되면 파일 수정 모드로 전환
|
|
1755
2637
|
if (detectPythonError(currentInput)) {
|
|
1756
|
-
console.log('[AgentPanel] Agent mode: No notebook, but Python error detected -
|
|
1757
|
-
//
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2638
|
+
console.log('[AgentPanel] Agent mode: No notebook, but Python error detected - attempting file fix');
|
|
2639
|
+
// Python 에러 수정 처리 시도
|
|
2640
|
+
const handled = await handlePythonErrorFix(currentInput);
|
|
2641
|
+
if (handled) {
|
|
2642
|
+
// 파일 수정 모드로 처리됨
|
|
2643
|
+
const userMessage = {
|
|
2644
|
+
id: makeMessageId(),
|
|
2645
|
+
role: 'user',
|
|
2646
|
+
content: currentInput,
|
|
2647
|
+
timestamp: Date.now(),
|
|
2648
|
+
};
|
|
2649
|
+
setMessages(prev => [...prev, userMessage]);
|
|
2650
|
+
setInput('');
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
// 파일 경로를 찾지 못함 - 아래 로직으로 계속 진행
|
|
2654
|
+
console.log('[AgentPanel] Agent mode: No file path found, continuing...');
|
|
1769
2655
|
}
|
|
1770
2656
|
// 파일 수정 관련 자연어 요청 감지 (에러, 고쳐, 수정, fix 등)
|
|
1771
2657
|
const fileFixRequestPatterns = [
|
|
@@ -1849,174 +2735,66 @@ SyntaxError: '(' was never closed
|
|
|
1849
2735
|
const userMessage = {
|
|
1850
2736
|
id: makeMessageId(),
|
|
1851
2737
|
role: 'user',
|
|
1852
|
-
content: currentInput,
|
|
1853
|
-
timestamp: Date.now(),
|
|
1854
|
-
};
|
|
1855
|
-
setMessages(prev => [...prev, userMessage]);
|
|
1856
|
-
setInput('');
|
|
1857
|
-
// Agent 실행
|
|
1858
|
-
await handleAgentExecution(agentRequest);
|
|
1859
|
-
return;
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
// Python 에러 감지 및 파일 수정 모드 (Chat 모드에서만)
|
|
1863
|
-
if (inputMode === 'chat' && detectPythonError(currentInput)) {
|
|
1864
|
-
console.log('[AgentPanel] Python error detected in message');
|
|
1865
|
-
// User 메시지 추가
|
|
1866
|
-
const userMessage = {
|
|
1867
|
-
id: makeMessageId(),
|
|
1868
|
-
role: 'user',
|
|
1869
|
-
content: currentInput,
|
|
1870
|
-
timestamp: Date.now(),
|
|
1871
|
-
};
|
|
1872
|
-
setMessages(prev => [...prev, userMessage]);
|
|
1873
|
-
setInput('');
|
|
1874
|
-
// Python 에러 수정 처리
|
|
1875
|
-
await handlePythonErrorFix(currentInput);
|
|
1876
|
-
return;
|
|
1877
|
-
}
|
|
1878
|
-
// Check if API key is configured before sending
|
|
1879
|
-
let currentConfig = llmConfig;
|
|
1880
|
-
if (!currentConfig) {
|
|
1881
|
-
// Config not loaded yet, try to load from localStorage
|
|
1882
|
-
currentConfig = (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getLLMConfig)() || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getDefaultLLMConfig)();
|
|
1883
|
-
setLlmConfig(currentConfig);
|
|
1884
|
-
}
|
|
1885
|
-
// Check API key using ApiKeyManager
|
|
1886
|
-
const hasApiKey = (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.hasValidApiKey)(currentConfig);
|
|
1887
|
-
if (!hasApiKey) {
|
|
1888
|
-
// Show error message and open settings
|
|
1889
|
-
const providerName = llmConfig?.provider || 'LLM';
|
|
1890
|
-
const errorMessage = {
|
|
1891
|
-
id: makeMessageId(),
|
|
1892
|
-
role: 'assistant',
|
|
1893
|
-
content: `API Key가 설정되지 않았습니다.\n\n${providerName === 'gemini' ? 'Gemini' : providerName === 'openai' ? 'OpenAI' : 'vLLM'} API Key를 먼저 설정해주세요.\n\n설정 버튼을 클릭하여 API Key를 입력하세요.`,
|
|
1894
|
-
timestamp: Date.now()
|
|
1895
|
-
};
|
|
1896
|
-
setMessages(prev => [...prev, errorMessage]);
|
|
1897
|
-
setShowSettings(true);
|
|
1898
|
-
return;
|
|
1899
|
-
}
|
|
1900
|
-
// Use the display prompt (input) for the user message, or use a fallback if input is empty
|
|
1901
|
-
const displayContent = currentInput || (llmPrompt ? '셀 분석 요청' : '');
|
|
1902
|
-
const userMessage = {
|
|
1903
|
-
id: makeMessageId(),
|
|
1904
|
-
role: 'user',
|
|
1905
|
-
content: displayContent,
|
|
1906
|
-
timestamp: Date.now()
|
|
1907
|
-
};
|
|
1908
|
-
setMessages(prev => [...prev, userMessage]);
|
|
1909
|
-
// Only clear input if it was manually entered, keep it for auto-execution display
|
|
1910
|
-
if (currentInput) {
|
|
1911
|
-
setInput('');
|
|
1912
|
-
}
|
|
1913
|
-
setIsLoading(true);
|
|
1914
|
-
setIsStreaming(true);
|
|
1915
|
-
// Clear the data attribute and ref after using it
|
|
1916
|
-
if (textarea && llmPrompt) {
|
|
1917
|
-
textarea.removeAttribute('data-llm-prompt');
|
|
1918
|
-
pendingLlmPromptRef.current = null;
|
|
1919
|
-
}
|
|
1920
|
-
// Create assistant message ID for streaming updates
|
|
1921
|
-
const assistantMessageId = makeMessageId('assistant');
|
|
1922
|
-
let streamedContent = '';
|
|
1923
|
-
setStreamingMessageId(assistantMessageId);
|
|
1924
|
-
// Add empty assistant message that will be updated during streaming
|
|
1925
|
-
const initialAssistantMessage = {
|
|
1926
|
-
id: assistantMessageId,
|
|
1927
|
-
role: 'assistant',
|
|
1928
|
-
content: '',
|
|
1929
|
-
timestamp: Date.now()
|
|
1930
|
-
};
|
|
1931
|
-
setMessages(prev => [...prev, initialAssistantMessage]);
|
|
1932
|
-
let interrupted = false;
|
|
1933
|
-
try {
|
|
1934
|
-
// Use LLM prompt if available, otherwise use the display content
|
|
1935
|
-
const messageToSend = llmPrompt || displayContent;
|
|
1936
|
-
await apiService.sendMessageStream({
|
|
1937
|
-
message: messageToSend,
|
|
1938
|
-
conversationId: conversationId || undefined,
|
|
1939
|
-
llmConfig: currentConfig // Include API keys with request
|
|
1940
|
-
},
|
|
1941
|
-
// onChunk callback - update message content incrementally
|
|
1942
|
-
(chunk) => {
|
|
1943
|
-
streamedContent += chunk;
|
|
1944
|
-
setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
|
|
1945
|
-
? { ...msg, content: streamedContent }
|
|
1946
|
-
: msg));
|
|
1947
|
-
},
|
|
1948
|
-
// onMetadata callback - update conversationId and metadata
|
|
1949
|
-
(metadata) => {
|
|
1950
|
-
if (metadata.conversationId && !conversationId) {
|
|
1951
|
-
setConversationId(metadata.conversationId);
|
|
1952
|
-
}
|
|
1953
|
-
if (metadata.provider || metadata.model) {
|
|
1954
|
-
setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
|
|
1955
|
-
? {
|
|
1956
|
-
...msg,
|
|
1957
|
-
metadata: {
|
|
1958
|
-
...msg.metadata,
|
|
1959
|
-
provider: metadata.provider,
|
|
1960
|
-
model: metadata.model
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
: msg));
|
|
1964
|
-
}
|
|
1965
|
-
},
|
|
1966
|
-
// onDebug callback - show debug status in gray
|
|
1967
|
-
(status) => {
|
|
1968
|
-
setDebugStatus(status);
|
|
1969
|
-
},
|
|
1970
|
-
// onInterrupt callback - show approval dialog
|
|
1971
|
-
(interrupt) => {
|
|
1972
|
-
interrupted = true;
|
|
1973
|
-
approvalPendingRef.current = true;
|
|
1974
|
-
if (interrupt.action === 'jupyter_cell_tool' && interrupt.args?.code) {
|
|
1975
|
-
if (isAutoApprovedCode(interrupt.args.code)) {
|
|
1976
|
-
queueApprovalCell(interrupt.args.code);
|
|
1977
|
-
void resumeFromInterrupt(interrupt, 'approve');
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
queueApprovalCell(interrupt.args.code);
|
|
1981
|
-
}
|
|
1982
|
-
setInterruptData(interrupt);
|
|
1983
|
-
upsertInterruptMessage(interrupt);
|
|
1984
|
-
setIsLoading(false);
|
|
1985
|
-
setIsStreaming(false);
|
|
1986
|
-
},
|
|
1987
|
-
// onTodos callback - update todo list UI
|
|
1988
|
-
(newTodos) => {
|
|
1989
|
-
setTodos(newTodos);
|
|
1990
|
-
},
|
|
1991
|
-
// onDebugClear callback - clear debug status
|
|
1992
|
-
() => {
|
|
1993
|
-
setDebugStatus(null);
|
|
1994
|
-
},
|
|
1995
|
-
// onToolCall callback - add cells to notebook
|
|
1996
|
-
handleToolCall);
|
|
1997
|
-
}
|
|
1998
|
-
catch (error) {
|
|
1999
|
-
const message = error instanceof Error ? error.message : 'Failed to send message';
|
|
2000
|
-
setDebugStatus(`오류: ${message}`);
|
|
2001
|
-
// Update the assistant message with error
|
|
2002
|
-
setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
|
|
2003
|
-
? {
|
|
2004
|
-
...msg,
|
|
2005
|
-
content: streamedContent + `\n\nError: ${message}`
|
|
2006
|
-
}
|
|
2007
|
-
: msg));
|
|
2738
|
+
content: currentInput,
|
|
2739
|
+
timestamp: Date.now(),
|
|
2740
|
+
};
|
|
2741
|
+
setMessages(prev => [...prev, userMessage]);
|
|
2742
|
+
setInput('');
|
|
2743
|
+
// Agent 실행
|
|
2744
|
+
await handleAgentExecution(agentRequest);
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2008
2747
|
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2748
|
+
// Python 에러 감지 및 파일 수정 모드 (Chat 모드에서만)
|
|
2749
|
+
// 파일 경로를 추출할 수 있는 경우에만 파일 수정 모드 사용
|
|
2750
|
+
// 그렇지 않으면 일반 LLM 스트림으로 처리
|
|
2751
|
+
if (inputMode === 'chat' && detectPythonError(currentInput)) {
|
|
2752
|
+
console.log('[AgentPanel] Python error detected in message, attempting file fix...');
|
|
2753
|
+
// Python 에러 수정 처리 시도 (파일 경로를 찾을 수 있는 경우에만 처리됨)
|
|
2754
|
+
const handled = await handlePythonErrorFix(currentInput);
|
|
2755
|
+
if (handled) {
|
|
2756
|
+
// 파일 수정 모드로 처리됨 - 사용자 메시지 추가 후 종료
|
|
2757
|
+
const userMessage = {
|
|
2758
|
+
id: makeMessageId(),
|
|
2759
|
+
role: 'user',
|
|
2760
|
+
content: currentInput,
|
|
2761
|
+
timestamp: Date.now(),
|
|
2762
|
+
};
|
|
2763
|
+
setMessages(prev => [...prev, userMessage]);
|
|
2764
|
+
setInput('');
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
// 파일 경로를 찾지 못해 handled=false 반환됨
|
|
2768
|
+
// 일반 스트림으로 폴백 (아래 로직에서 사용자 메시지 추가됨)
|
|
2769
|
+
console.log('[AgentPanel] No file path found in error, using regular LLM stream');
|
|
2014
2770
|
}
|
|
2771
|
+
// Use the display prompt (input) for the user message, or use a fallback if input is empty
|
|
2772
|
+
const displayContent = currentInput || (llmPrompt ? '셀 분석 요청' : '');
|
|
2773
|
+
await sendChatMessage({
|
|
2774
|
+
displayContent,
|
|
2775
|
+
llmPrompt,
|
|
2776
|
+
textarea,
|
|
2777
|
+
clearInput: Boolean(currentInput)
|
|
2778
|
+
});
|
|
2015
2779
|
};
|
|
2016
2780
|
// Handle resume after user approval/rejection
|
|
2017
2781
|
const handleResumeAgent = async (decision) => {
|
|
2018
2782
|
if (!interruptData)
|
|
2019
2783
|
return;
|
|
2784
|
+
if (decision === 'reject') {
|
|
2785
|
+
// Enter rejection mode - wait for user feedback before resuming
|
|
2786
|
+
setIsRejectionMode(true);
|
|
2787
|
+
setPendingRejectionInterrupt(interruptData);
|
|
2788
|
+
// Clear the interrupt UI but keep the data for later, mark as rejected
|
|
2789
|
+
setInterruptData(null);
|
|
2790
|
+
clearInterruptMessage('reject'); // Pass 'reject' to show "거부됨"
|
|
2791
|
+
// Focus on input for user to provide feedback
|
|
2792
|
+
const textarea = document.querySelector('.jp-agent-input');
|
|
2793
|
+
if (textarea) {
|
|
2794
|
+
textarea.focus();
|
|
2795
|
+
}
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2020
2798
|
await resumeFromInterrupt(interruptData, decision);
|
|
2021
2799
|
};
|
|
2022
2800
|
const handleKeyDown = (e) => {
|
|
@@ -2143,9 +2921,6 @@ SyntaxError: '(' was never closed
|
|
|
2143
2921
|
if (normalized.includes('resuming execution')) {
|
|
2144
2922
|
return '승인 반영 후 실행 재개 중...';
|
|
2145
2923
|
}
|
|
2146
|
-
if (normalized.includes('tool')) {
|
|
2147
|
-
return '도구 실행 중';
|
|
2148
|
-
}
|
|
2149
2924
|
return raw;
|
|
2150
2925
|
};
|
|
2151
2926
|
const getStatusText = () => {
|
|
@@ -2162,6 +2937,7 @@ SyntaxError: '(' was never closed
|
|
|
2162
2937
|
return null;
|
|
2163
2938
|
};
|
|
2164
2939
|
const statusText = getStatusText();
|
|
2940
|
+
const hasActiveTodos = todos.some(todo => todo.status === 'pending' || todo.status === 'in_progress');
|
|
2165
2941
|
return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-panel" },
|
|
2166
2942
|
showSettings && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_SettingsPanel__WEBPACK_IMPORTED_MODULE_3__.SettingsPanel, { onClose: () => setShowSettings(false), onSave: handleSaveConfig, currentConfig: llmConfig || undefined })),
|
|
2167
2943
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-header" },
|
|
@@ -2193,30 +2969,51 @@ SyntaxError: '(' was never closed
|
|
|
2193
2969
|
if (isChatMessage(msg)) {
|
|
2194
2970
|
// 일반 Chat 메시지
|
|
2195
2971
|
const isAssistant = msg.role === 'assistant';
|
|
2972
|
+
const isShellOutput = msg.metadata?.kind === 'shell-output';
|
|
2973
|
+
const interruptAction = msg.metadata?.interrupt?.action;
|
|
2974
|
+
const isWriteFile = interruptAction === 'write_file_tool';
|
|
2975
|
+
const writePath = (isWriteFile
|
|
2976
|
+
&& typeof msg.metadata?.interrupt?.args?.path === 'string') ? msg.metadata?.interrupt?.args?.path : '';
|
|
2977
|
+
const headerRole = msg.role === 'user'
|
|
2978
|
+
? '사용자'
|
|
2979
|
+
: msg.role === 'system'
|
|
2980
|
+
? (isShellOutput ? 'shell 실행' : '승인 요청')
|
|
2981
|
+
: 'Agent';
|
|
2196
2982
|
return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { key: msg.id, className: isAssistant
|
|
2197
2983
|
? 'jp-agent-message jp-agent-message-assistant-inline'
|
|
2198
|
-
: `jp-agent-message jp-agent-message-${msg.role}` },
|
|
2984
|
+
: `jp-agent-message jp-agent-message-${msg.role}${isShellOutput ? ' jp-agent-message-shell-output' : ''}` },
|
|
2199
2985
|
!isAssistant && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-message-header" },
|
|
2200
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-message-role" },
|
|
2986
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-message-role" }, headerRole),
|
|
2201
2987
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-message-time" }, new Date(msg.timestamp).toLocaleTimeString()))),
|
|
2202
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message-content${streamingMessageId === msg.id ? ' streaming' : ''}` }, msg.role === 'system' && msg.metadata?.interrupt ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-inline" },
|
|
2988
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message-content${streamingMessageId === msg.id ? ' streaming' : ''}${isShellOutput ? ' jp-agent-message-content-shell' : ''}` }, msg.role === 'system' && msg.metadata?.interrupt ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-inline" },
|
|
2203
2989
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-description" }, msg.content),
|
|
2204
2990
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-action" },
|
|
2205
2991
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-action-args" }, (() => {
|
|
2992
|
+
const command = msg.metadata?.interrupt?.args?.command;
|
|
2206
2993
|
const code = msg.metadata?.interrupt?.args?.code || msg.metadata?.interrupt?.args?.content || '';
|
|
2207
|
-
const
|
|
2208
|
-
const
|
|
2209
|
-
const suffix = lines.length > 8 ? '\n...' : '';
|
|
2994
|
+
const snippet = (command || code || '(no details)');
|
|
2995
|
+
const language = command ? 'bash' : 'python';
|
|
2210
2996
|
const resolved = msg.metadata?.interrupt?.resolved;
|
|
2997
|
+
const decision = msg.metadata?.interrupt?.decision;
|
|
2998
|
+
const resolvedText = decision === 'reject' ? '거부됨' : '승인됨';
|
|
2999
|
+
const resolvedClass = decision === 'reject' ? 'jp-agent-interrupt-actions--rejected' : 'jp-agent-interrupt-actions--resolved';
|
|
2211
3000
|
const actionHtml = resolved
|
|
2212
|
-
?
|
|
3001
|
+
? `<div class="jp-agent-interrupt-actions ${resolvedClass}">${resolvedText}</div>`
|
|
2213
3002
|
: `
|
|
2214
3003
|
<div class="code-block-actions jp-agent-interrupt-actions">
|
|
2215
3004
|
<button class="jp-agent-interrupt-approve-btn" data-action="approve">승인</button>
|
|
2216
3005
|
<button class="jp-agent-interrupt-reject-btn" data-action="reject">거부</button>
|
|
2217
3006
|
</div>
|
|
2218
3007
|
`;
|
|
2219
|
-
|
|
3008
|
+
const renderedHtml = (() => {
|
|
3009
|
+
let html = (0,_utils_markdownRenderer__WEBPACK_IMPORTED_MODULE_7__.formatMarkdownToHtml)(`\n\`\`\`${language}\n${snippet}\n\`\`\``);
|
|
3010
|
+
if (isWriteFile && writePath) {
|
|
3011
|
+
const safePath = escapeHtml(writePath);
|
|
3012
|
+
html = html.replace(/<span class="code-block-language">[^<]*<\/span>/, `<span class="code-block-language jp-agent-interrupt-path">${safePath}</span>`);
|
|
3013
|
+
}
|
|
3014
|
+
return html.replace('</div>', `${actionHtml}</div>`);
|
|
3015
|
+
})();
|
|
3016
|
+
return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-RenderedHTMLCommon", style: { padding: '0 4px' }, dangerouslySetInnerHTML: { __html: renderedHtml }, onClick: (event) => {
|
|
2220
3017
|
const target = event.target;
|
|
2221
3018
|
const action = target?.getAttribute?.('data-action');
|
|
2222
3019
|
if (msg.metadata?.interrupt?.resolved) {
|
|
@@ -2276,11 +3073,6 @@ SyntaxError: '(' was never closed
|
|
|
2276
3073
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: "jp-agent-file-fix-apply", onClick: () => applyFileFix(fix), title: `${fix.path} 파일에 수정 적용` }, "\uC801\uC6A9\uD558\uAE30"))))),
|
|
2277
3074
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: "jp-agent-file-fixes-dismiss", onClick: () => setPendingFileFixes([]), title: "\uC218\uC815 \uC81C\uC548 \uB2EB\uAE30" }, "\uB2EB\uAE30"))),
|
|
2278
3075
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { ref: messagesEndRef })),
|
|
2279
|
-
statusText && todos.length === 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message jp-agent-message-debug${statusText.startsWith('오류:') ? ' jp-agent-message-debug-error' : ''}` },
|
|
2280
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-content" },
|
|
2281
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-branch", "aria-hidden": "true" }),
|
|
2282
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-text" }, statusText),
|
|
2283
|
-
!statusText.startsWith('오류:') && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-ellipsis", "aria-hidden": "true" }))))),
|
|
2284
3076
|
todos.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact" },
|
|
2285
3077
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-header", onClick: () => setIsTodoExpanded(!isTodoExpanded) },
|
|
2286
3078
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-left" },
|
|
@@ -2288,15 +3080,26 @@ SyntaxError: '(' was never closed
|
|
|
2288
3080
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("path", { d: "M6 12l4-4-4-4" })),
|
|
2289
3081
|
(() => {
|
|
2290
3082
|
const currentTodo = todos.find(t => t.status === 'in_progress') || todos.find(t => t.status === 'pending');
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement(
|
|
3083
|
+
const isStillWorking = isStreaming || isLoading || isAgentRunning;
|
|
3084
|
+
if (currentTodo) {
|
|
3085
|
+
return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
|
|
3086
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-spinner" }),
|
|
3087
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-current" }, currentTodo.content)));
|
|
3088
|
+
}
|
|
3089
|
+
else if (isStillWorking) {
|
|
3090
|
+
return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
|
|
3091
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-spinner" }),
|
|
3092
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-current" }, "\uC791\uC5C5 \uB9C8\uBB34\uB9AC \uC911...")));
|
|
3093
|
+
}
|
|
3094
|
+
else {
|
|
3095
|
+
return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-current" }, "\u2713 \uBAA8\uB4E0 \uC791\uC5C5 \uC644\uB8CC"));
|
|
3096
|
+
}
|
|
2294
3097
|
})()),
|
|
2295
3098
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-progress" },
|
|
2296
3099
|
todos.filter(t => t.status === 'completed').length,
|
|
2297
3100
|
"/",
|
|
2298
3101
|
todos.length)),
|
|
2299
|
-
statusText && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message jp-agent-message-debug jp-agent-message-debug--inline${statusText.startsWith('오류:') ? ' jp-agent-message-debug-error' : ''}` },
|
|
3102
|
+
statusText && hasActiveTodos && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message jp-agent-message-debug jp-agent-message-debug--inline${statusText.startsWith('오류:') ? ' jp-agent-message-debug-error' : ''}` },
|
|
2300
3103
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-content" },
|
|
2301
3104
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-branch", "aria-hidden": "true" }),
|
|
2302
3105
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-text" }, statusText),
|
|
@@ -2308,12 +3111,18 @@ SyntaxError: '(' was never closed
|
|
|
2308
3111
|
todo.status === 'in_progress' && react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-item-spinner" }),
|
|
2309
3112
|
todo.status === 'pending' && react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-item-number" }, index + 1)),
|
|
2310
3113
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: `jp-agent-todo-item-text ${todo.status === 'completed' ? 'jp-agent-todo-item-text--done' : ''}` }, todo.content)))))))),
|
|
3114
|
+
statusText && !hasActiveTodos && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message jp-agent-message-debug jp-agent-message-debug--above-input${statusText.startsWith('오류:') ? ' jp-agent-message-debug-error' : ''}` },
|
|
3115
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-content" },
|
|
3116
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-text" }, statusText),
|
|
3117
|
+
!statusText.startsWith('오류:') && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-ellipsis", "aria-hidden": "true" }))))),
|
|
2311
3118
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-input-container" },
|
|
2312
3119
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-input-wrapper" },
|
|
2313
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("textarea", { className: `jp-agent-input ${inputMode === 'agent' ? 'jp-agent-input--agent-mode' : ''}`, value: input, onChange: (e) => setInput(e.target.value), onKeyDown: handleKeyDown, placeholder:
|
|
2314
|
-
? '
|
|
2315
|
-
:
|
|
2316
|
-
|
|
3120
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("textarea", { className: `jp-agent-input ${inputMode === 'agent' ? 'jp-agent-input--agent-mode' : ''} ${isRejectionMode ? 'jp-agent-input--rejection-mode' : ''}`, value: input, onChange: (e) => setInput(e.target.value), onKeyDown: handleKeyDown, placeholder: isRejectionMode
|
|
3121
|
+
? '다른 방향 제시'
|
|
3122
|
+
: (inputMode === 'agent'
|
|
3123
|
+
? '노트북 작업을 입력하세요... (예: 데이터 시각화 해줘)'
|
|
3124
|
+
: '메시지를 입력하세요...'), rows: 3, disabled: isLoading || isAgentRunning }),
|
|
3125
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: "jp-agent-send-button", onClick: handleSendMessage, disabled: (!input.trim() && !isRejectionMode) || isLoading || isStreaming || isAgentRunning, title: isRejectionMode ? "거부 전송 (Enter)" : "전송 (Enter)" }, isAgentRunning ? '실행 중...' : (isRejectionMode ? '거부' : '전송'))),
|
|
2317
3126
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-bar" },
|
|
2318
3127
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-toggle-container" },
|
|
2319
3128
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: `jp-agent-mode-toggle ${inputMode === 'agent' ? 'jp-agent-mode-toggle--active' : ''}`, onClick: toggleMode, title: `${inputMode === 'agent' ? 'Agent' : 'Chat'} 모드 (⇧Tab)` },
|
|
@@ -2830,6 +3639,8 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
|
|
|
2830
3639
|
const [openaiApiKey, setOpenaiApiKey] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.openai?.apiKey || '');
|
|
2831
3640
|
const [openaiModel, setOpenaiModel] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.openai?.model || 'gpt-4');
|
|
2832
3641
|
const [systemPrompt, setSystemPrompt] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.systemPrompt || '');
|
|
3642
|
+
const [workspaceRoot, setWorkspaceRoot] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.workspaceRoot || '');
|
|
3643
|
+
const [autoApprove, setAutoApprove] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(Boolean(initConfig.autoApprove));
|
|
2833
3644
|
// Update state when currentConfig changes
|
|
2834
3645
|
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
|
|
2835
3646
|
if (currentConfig) {
|
|
@@ -2851,6 +3662,8 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
|
|
|
2851
3662
|
setOpenaiApiKey(currentConfig.openai?.apiKey || '');
|
|
2852
3663
|
setOpenaiModel(currentConfig.openai?.model || 'gpt-4');
|
|
2853
3664
|
setSystemPrompt(currentConfig.systemPrompt || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_1__.getDefaultLLMConfig)().systemPrompt || '');
|
|
3665
|
+
setWorkspaceRoot(currentConfig.workspaceRoot || '');
|
|
3666
|
+
setAutoApprove(Boolean(currentConfig.autoApprove));
|
|
2854
3667
|
}
|
|
2855
3668
|
}, [currentConfig]);
|
|
2856
3669
|
// Helper: Build LLM config from state
|
|
@@ -2870,7 +3683,9 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
|
|
|
2870
3683
|
apiKey: openaiApiKey,
|
|
2871
3684
|
model: openaiModel
|
|
2872
3685
|
},
|
|
2873
|
-
|
|
3686
|
+
workspaceRoot: workspaceRoot.trim() ? workspaceRoot.trim() : undefined,
|
|
3687
|
+
systemPrompt: systemPrompt && systemPrompt.trim() ? systemPrompt : undefined,
|
|
3688
|
+
autoApprove
|
|
2874
3689
|
});
|
|
2875
3690
|
// Handlers for multiple API keys
|
|
2876
3691
|
const handleAddKey = () => {
|
|
@@ -2964,6 +3779,11 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
|
|
|
2964
3779
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "gemini" }, "Google Gemini"),
|
|
2965
3780
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "vllm" }, "vLLM"),
|
|
2966
3781
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "openai" }, "OpenAI"))),
|
|
3782
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
|
|
3783
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label", htmlFor: "jp-agent-workspace-root" },
|
|
3784
|
+
"\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uB8E8\uD2B8",
|
|
3785
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("small", { style: { fontWeight: 'normal', marginLeft: '8px', color: '#666' } }, "\uBE44\uC6B0\uBA74 \uD604\uC7AC \uB178\uD2B8\uBD81 \uD3F4\uB354 \uAE30\uC900, \uC808\uB300/\uC0C1\uB300 \uACBD\uB85C \uAC00\uB2A5")),
|
|
3786
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { id: "jp-agent-workspace-root", type: "text", className: "jp-agent-settings-input", value: workspaceRoot, onChange: (e) => setWorkspaceRoot(e.target.value), placeholder: "\uC608: /Users/you/project", "data-testid": "workspace-root-input" })),
|
|
2967
3787
|
provider === 'gemini' && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-provider" },
|
|
2968
3788
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h3", null, "Gemini \uC124\uC815"),
|
|
2969
3789
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
|
|
@@ -3028,6 +3848,11 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
|
|
|
3028
3848
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "gpt-4" }, "GPT-4"),
|
|
3029
3849
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "gpt-4-turbo" }, "GPT-4 Turbo"),
|
|
3030
3850
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "gpt-3.5-turbo" }, "GPT-3.5 Turbo"))))),
|
|
3851
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
|
|
3852
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label" }, "\uC790\uB3D9 \uC2E4\uD589 \uC2B9\uC778"),
|
|
3853
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-checkbox" },
|
|
3854
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { type: "checkbox", checked: autoApprove, onChange: (e) => setAutoApprove(e.target.checked), "data-testid": "auto-approve-checkbox" }),
|
|
3855
|
+
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "\uC2B9\uC778 \uC5C6\uC774 \uBC14\uB85C \uC2E4\uD589 (\uCF54\uB4DC/\uD30C\uC77C/\uC178 \uD3EC\uD568)"))),
|
|
3031
3856
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
|
|
3032
3857
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label" },
|
|
3033
3858
|
"System Prompt (LangChain)",
|
|
@@ -6908,34 +7733,38 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
6908
7733
|
*/
|
|
6909
7734
|
const STORAGE_KEY = 'hdsp-agent-llm-config';
|
|
6910
7735
|
const DEFAULT_LANGCHAIN_SYSTEM_PROMPT = `You are an expert Python data scientist and Jupyter notebook assistant.
|
|
6911
|
-
Your role is to help users with data analysis, visualization, and Python coding tasks in Jupyter notebooks.
|
|
7736
|
+
Your role is to help users with data analysis, visualization, and Python coding tasks in Jupyter notebooks. You can use only Korean
|
|
6912
7737
|
|
|
6913
7738
|
## ⚠️ CRITICAL RULE: NEVER produce an empty response
|
|
6914
7739
|
|
|
6915
7740
|
You MUST ALWAYS call a tool in every response. After any tool result, you MUST:
|
|
6916
7741
|
1. Check your todo list - are there pending or in_progress items?
|
|
6917
7742
|
2. If YES → call the next appropriate tool (jupyter_cell_tool, markdown_tool, etc.)
|
|
6918
|
-
3.
|
|
7743
|
+
3. When you suggest next steps for todo item '다음 단계 제시', you MUST create next steps in json format matching this schema:
|
|
7744
|
+
{
|
|
7745
|
+
"next_items": [
|
|
7746
|
+
{
|
|
7747
|
+
"subject": "<subject for next step>",
|
|
7748
|
+
"description": "<detailed description for the next step>"
|
|
7749
|
+
}, ...
|
|
7750
|
+
]
|
|
7751
|
+
}
|
|
7752
|
+
4. If ALL todos are completed → call final_answer_tool with a summary
|
|
6919
7753
|
|
|
6920
7754
|
NEVER end your turn without calling a tool. NEVER produce an empty response.
|
|
6921
7755
|
|
|
6922
|
-
##
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
5. **write_file_tool**: Write file contents
|
|
6928
|
-
6. **list_files_tool**: List directory contents
|
|
6929
|
-
7. **search_workspace_tool**: Search for patterns in workspace files
|
|
6930
|
-
8. **search_notebook_cells_tool**: Search for patterns in notebook cells
|
|
6931
|
-
9. **write_todos**: Create and update task list for complex multi-step tasks
|
|
7756
|
+
## 🔴 MANDATORY: Resource Check Before Data Hanlding
|
|
7757
|
+
**ALWAYS call check_resource_tool FIRST** when the task involves:
|
|
7758
|
+
- Loading files: .csv, .parquet, .json, .xlsx, .pickle, .h5, .feather
|
|
7759
|
+
- Handling datasets(dataframe) with pandas, polars, dask, or similar libraries
|
|
7760
|
+
- Training ML models on data files
|
|
6932
7761
|
|
|
6933
7762
|
## Mandatory Workflow
|
|
6934
7763
|
1. After EVERY tool result, immediately call the next tool
|
|
6935
7764
|
2. Continue until ALL todos show status: "completed"
|
|
6936
7765
|
3. ONLY THEN call final_answer_tool to summarize
|
|
6937
|
-
4.
|
|
6938
|
-
5. For plots and charts, use English text only
|
|
7766
|
+
4. Only use jupyter_cell_tool for Python code or when the user explicitly asks to run in a notebook cell
|
|
7767
|
+
5. For plots and charts, use English text only.
|
|
6939
7768
|
|
|
6940
7769
|
## ❌ FORBIDDEN (will break the workflow)
|
|
6941
7770
|
- Producing an empty response (no tool call, no content)
|
|
@@ -7027,7 +7856,8 @@ function getDefaultLLMConfig() {
|
|
|
7027
7856
|
endpoint: 'http://localhost:8000',
|
|
7028
7857
|
model: 'default'
|
|
7029
7858
|
},
|
|
7030
|
-
systemPrompt: DEFAULT_LANGCHAIN_SYSTEM_PROMPT
|
|
7859
|
+
systemPrompt: DEFAULT_LANGCHAIN_SYSTEM_PROMPT,
|
|
7860
|
+
autoApprove: false
|
|
7031
7861
|
};
|
|
7032
7862
|
}
|
|
7033
7863
|
/**
|
|
@@ -7254,6 +8084,8 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
7254
8084
|
class ApiService {
|
|
7255
8085
|
// 생성자에서 baseUrl을 선택적으로 받도록 하되, 없으면 자동으로 계산
|
|
7256
8086
|
constructor(baseUrl) {
|
|
8087
|
+
this.resourceUsageCache = null;
|
|
8088
|
+
this.resourceUsageCacheMs = 15000;
|
|
7257
8089
|
if (baseUrl) {
|
|
7258
8090
|
this.baseUrl = baseUrl;
|
|
7259
8091
|
}
|
|
@@ -7293,6 +8125,35 @@ class ApiService {
|
|
|
7293
8125
|
'X-XSRFToken': this.getCsrfToken()
|
|
7294
8126
|
};
|
|
7295
8127
|
}
|
|
8128
|
+
async getResourceUsageSnapshot() {
|
|
8129
|
+
const now = Date.now();
|
|
8130
|
+
if (this.resourceUsageCache
|
|
8131
|
+
&& now - this.resourceUsageCache.timestamp < this.resourceUsageCacheMs) {
|
|
8132
|
+
return this.resourceUsageCache.resource;
|
|
8133
|
+
}
|
|
8134
|
+
try {
|
|
8135
|
+
const response = await fetch(`${this.baseUrl}/resource-usage`, {
|
|
8136
|
+
method: 'GET',
|
|
8137
|
+
credentials: 'include'
|
|
8138
|
+
});
|
|
8139
|
+
if (!response.ok) {
|
|
8140
|
+
return null;
|
|
8141
|
+
}
|
|
8142
|
+
const payload = await response.json().catch(() => ({}));
|
|
8143
|
+
const candidate = payload?.resource ?? payload;
|
|
8144
|
+
const snapshot = candidate && typeof candidate === 'object' && !Array.isArray(candidate)
|
|
8145
|
+
? candidate
|
|
8146
|
+
: null;
|
|
8147
|
+
if (snapshot) {
|
|
8148
|
+
this.resourceUsageCache = { resource: snapshot, timestamp: now };
|
|
8149
|
+
}
|
|
8150
|
+
return snapshot;
|
|
8151
|
+
}
|
|
8152
|
+
catch (error) {
|
|
8153
|
+
console.warn('[ApiService] Resource usage fetch failed:', error);
|
|
8154
|
+
return null;
|
|
8155
|
+
}
|
|
8156
|
+
}
|
|
7296
8157
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
7297
8158
|
// Global Rate Limit Handling with Key Rotation
|
|
7298
8159
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -7401,7 +8262,9 @@ class ApiService {
|
|
|
7401
8262
|
* - Server receives ONLY ONE key per request
|
|
7402
8263
|
* - On 429 rate limit, frontend rotates key and retries with next key
|
|
7403
8264
|
*/
|
|
7404
|
-
async sendMessageStream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall
|
|
8265
|
+
async sendMessageStream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, // Callback to capture thread_id for context persistence
|
|
8266
|
+
threadId // Optional thread_id to continue existing conversation
|
|
8267
|
+
) {
|
|
7405
8268
|
// Maximum retry attempts (should match number of keys)
|
|
7406
8269
|
const MAX_RETRIES = 10;
|
|
7407
8270
|
let currentConfig = request.llmConfig;
|
|
@@ -7412,7 +8275,7 @@ class ApiService {
|
|
|
7412
8275
|
? { ...request, llmConfig: (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.buildSingleKeyConfig)(currentConfig) }
|
|
7413
8276
|
: request;
|
|
7414
8277
|
try {
|
|
7415
|
-
await this.sendMessageStreamInternal(requestToSend, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall);
|
|
8278
|
+
await this.sendMessageStreamInternal(requestToSend, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId);
|
|
7416
8279
|
// Success - reset key rotation state
|
|
7417
8280
|
(0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.resetKeyRotation)();
|
|
7418
8281
|
return;
|
|
@@ -7462,11 +8325,16 @@ class ApiService {
|
|
|
7462
8325
|
* Internal streaming implementation (without retry logic)
|
|
7463
8326
|
* Uses LangChain agent endpoint for improved middleware support
|
|
7464
8327
|
*/
|
|
7465
|
-
async sendMessageStreamInternal(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall) {
|
|
8328
|
+
async sendMessageStreamInternal(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId) {
|
|
7466
8329
|
// Convert IChatRequest to LangChain AgentRequest format
|
|
7467
8330
|
// Frontend's context has limited fields, map what's available
|
|
8331
|
+
const resourceContext = await this.getResourceUsageSnapshot();
|
|
8332
|
+
const requestConfig = resourceContext && request.llmConfig
|
|
8333
|
+
? { ...request.llmConfig, resourceContext }
|
|
8334
|
+
: request.llmConfig;
|
|
7468
8335
|
const langchainRequest = {
|
|
7469
8336
|
request: request.message,
|
|
8337
|
+
threadId: threadId,
|
|
7470
8338
|
notebookContext: request.context ? {
|
|
7471
8339
|
notebook_path: request.context.notebookPath,
|
|
7472
8340
|
cell_count: 0,
|
|
@@ -7474,15 +8342,22 @@ class ApiService {
|
|
|
7474
8342
|
defined_variables: [],
|
|
7475
8343
|
recent_cells: request.context.selectedCells?.map(cell => ({ source: cell })) || []
|
|
7476
8344
|
} : undefined,
|
|
7477
|
-
llmConfig:
|
|
8345
|
+
llmConfig: requestConfig,
|
|
7478
8346
|
workspaceRoot: '.'
|
|
7479
8347
|
};
|
|
8348
|
+
// Debug: log request size
|
|
8349
|
+
const requestBody = JSON.stringify(langchainRequest);
|
|
8350
|
+
console.log('[ApiService] Sending langchain request:', {
|
|
8351
|
+
messageLength: request.message.length,
|
|
8352
|
+
bodyLength: requestBody.length,
|
|
8353
|
+
threadId: threadId,
|
|
8354
|
+
});
|
|
7480
8355
|
// Use LangChain streaming endpoint
|
|
7481
8356
|
const response = await fetch(`${this.baseUrl}/agent/langchain/stream`, {
|
|
7482
8357
|
method: 'POST',
|
|
7483
8358
|
headers: this.getHeaders(),
|
|
7484
8359
|
credentials: 'include',
|
|
7485
|
-
body:
|
|
8360
|
+
body: requestBody
|
|
7486
8361
|
});
|
|
7487
8362
|
if (!response.ok) {
|
|
7488
8363
|
const error = await response.text();
|
|
@@ -7529,6 +8404,15 @@ class ApiService {
|
|
|
7529
8404
|
if (onDebugClear) {
|
|
7530
8405
|
onDebugClear();
|
|
7531
8406
|
}
|
|
8407
|
+
// Capture thread_id for context persistence across cycles
|
|
8408
|
+
console.log('[ApiService] Complete event received, data:', data);
|
|
8409
|
+
if (onComplete && data.thread_id) {
|
|
8410
|
+
console.log('[ApiService] Calling onComplete with threadId:', data.thread_id);
|
|
8411
|
+
onComplete({ threadId: data.thread_id });
|
|
8412
|
+
}
|
|
8413
|
+
else {
|
|
8414
|
+
console.log('[ApiService] onComplete not called - onComplete:', !!onComplete, 'thread_id:', data.thread_id);
|
|
8415
|
+
}
|
|
7532
8416
|
return;
|
|
7533
8417
|
}
|
|
7534
8418
|
// Handle errors
|
|
@@ -7561,7 +8445,9 @@ class ApiService {
|
|
|
7561
8445
|
onToolCall({
|
|
7562
8446
|
tool: data.tool,
|
|
7563
8447
|
code: data.code,
|
|
7564
|
-
content: data.content
|
|
8448
|
+
content: data.content,
|
|
8449
|
+
command: data.command,
|
|
8450
|
+
timeout: data.timeout
|
|
7565
8451
|
});
|
|
7566
8452
|
currentEventType = '';
|
|
7567
8453
|
continue;
|
|
@@ -7612,6 +8498,10 @@ class ApiService {
|
|
|
7612
8498
|
* Resume interrupted agent execution with user decision
|
|
7613
8499
|
*/
|
|
7614
8500
|
async resumeAgent(threadId, decision, args, feedback, llmConfig, onChunk, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall) {
|
|
8501
|
+
const resourceContext = await this.getResourceUsageSnapshot();
|
|
8502
|
+
const requestConfig = resourceContext && llmConfig
|
|
8503
|
+
? { ...llmConfig, resourceContext }
|
|
8504
|
+
: llmConfig;
|
|
7615
8505
|
const resumeRequest = {
|
|
7616
8506
|
threadId,
|
|
7617
8507
|
decisions: [{
|
|
@@ -7619,7 +8509,7 @@ class ApiService {
|
|
|
7619
8509
|
args,
|
|
7620
8510
|
feedback
|
|
7621
8511
|
}],
|
|
7622
|
-
llmConfig,
|
|
8512
|
+
llmConfig: requestConfig,
|
|
7623
8513
|
workspaceRoot: '.'
|
|
7624
8514
|
};
|
|
7625
8515
|
const response = await fetch(`${this.baseUrl}/agent/langchain/resume`, {
|
|
@@ -7704,7 +8594,9 @@ class ApiService {
|
|
|
7704
8594
|
onToolCall({
|
|
7705
8595
|
tool: data.tool,
|
|
7706
8596
|
code: data.code,
|
|
7707
|
-
content: data.content
|
|
8597
|
+
content: data.content,
|
|
8598
|
+
command: data.command,
|
|
8599
|
+
timeout: data.timeout
|
|
7708
8600
|
});
|
|
7709
8601
|
currentEventType = '';
|
|
7710
8602
|
continue;
|
|
@@ -7724,6 +8616,194 @@ class ApiService {
|
|
|
7724
8616
|
reader.releaseLock();
|
|
7725
8617
|
}
|
|
7726
8618
|
}
|
|
8619
|
+
async executeCommand(command, timeout, cwd, stdin) {
|
|
8620
|
+
const response = await fetch(`${this.baseUrl}/execute-command`, {
|
|
8621
|
+
method: 'POST',
|
|
8622
|
+
headers: this.getHeaders(),
|
|
8623
|
+
credentials: 'include',
|
|
8624
|
+
body: JSON.stringify({ command, timeout, cwd, stdin })
|
|
8625
|
+
});
|
|
8626
|
+
const payload = await response.json().catch(() => ({}));
|
|
8627
|
+
if (!response.ok) {
|
|
8628
|
+
const errorMessage = payload.error || 'Failed to execute command';
|
|
8629
|
+
throw new Error(errorMessage);
|
|
8630
|
+
}
|
|
8631
|
+
return payload;
|
|
8632
|
+
}
|
|
8633
|
+
async executeCommandStream(command, options) {
|
|
8634
|
+
const response = await fetch(`${this.baseUrl}/execute-command/stream`, {
|
|
8635
|
+
method: 'POST',
|
|
8636
|
+
headers: this.getHeaders(),
|
|
8637
|
+
credentials: 'include',
|
|
8638
|
+
body: JSON.stringify({
|
|
8639
|
+
command,
|
|
8640
|
+
timeout: options?.timeout,
|
|
8641
|
+
cwd: options?.cwd,
|
|
8642
|
+
stdin: options?.stdin
|
|
8643
|
+
})
|
|
8644
|
+
});
|
|
8645
|
+
if (!response.ok) {
|
|
8646
|
+
const payload = await response.json().catch(() => ({}));
|
|
8647
|
+
const errorMessage = payload.error || 'Failed to execute command';
|
|
8648
|
+
throw new Error(errorMessage);
|
|
8649
|
+
}
|
|
8650
|
+
const reader = response.body?.getReader();
|
|
8651
|
+
if (!reader) {
|
|
8652
|
+
throw new Error('Response body is not readable');
|
|
8653
|
+
}
|
|
8654
|
+
const decoder = new TextDecoder();
|
|
8655
|
+
let buffer = '';
|
|
8656
|
+
let result = null;
|
|
8657
|
+
let streamError = null;
|
|
8658
|
+
try {
|
|
8659
|
+
while (true) {
|
|
8660
|
+
const { done, value } = await reader.read();
|
|
8661
|
+
if (done)
|
|
8662
|
+
break;
|
|
8663
|
+
buffer += decoder.decode(value, { stream: true });
|
|
8664
|
+
const lines = buffer.split('\n');
|
|
8665
|
+
buffer = lines.pop() || '';
|
|
8666
|
+
let currentEventType = '';
|
|
8667
|
+
for (const line of lines) {
|
|
8668
|
+
if (line.startsWith('event: ')) {
|
|
8669
|
+
currentEventType = line.slice(7).trim();
|
|
8670
|
+
continue;
|
|
8671
|
+
}
|
|
8672
|
+
if (!line.startsWith('data: ')) {
|
|
8673
|
+
continue;
|
|
8674
|
+
}
|
|
8675
|
+
let data;
|
|
8676
|
+
try {
|
|
8677
|
+
data = JSON.parse(line.slice(6));
|
|
8678
|
+
}
|
|
8679
|
+
catch (e) {
|
|
8680
|
+
continue;
|
|
8681
|
+
}
|
|
8682
|
+
if (currentEventType === 'output') {
|
|
8683
|
+
if (typeof data.text === 'string' && options?.onOutput) {
|
|
8684
|
+
options.onOutput({
|
|
8685
|
+
stream: data.stream === 'stderr' ? 'stderr' : 'stdout',
|
|
8686
|
+
text: data.text
|
|
8687
|
+
});
|
|
8688
|
+
}
|
|
8689
|
+
currentEventType = '';
|
|
8690
|
+
continue;
|
|
8691
|
+
}
|
|
8692
|
+
if (currentEventType === 'error') {
|
|
8693
|
+
streamError = data.error || 'Command execution failed';
|
|
8694
|
+
currentEventType = '';
|
|
8695
|
+
continue;
|
|
8696
|
+
}
|
|
8697
|
+
if (currentEventType === 'result') {
|
|
8698
|
+
result = data;
|
|
8699
|
+
return result;
|
|
8700
|
+
}
|
|
8701
|
+
currentEventType = '';
|
|
8702
|
+
}
|
|
8703
|
+
}
|
|
8704
|
+
}
|
|
8705
|
+
finally {
|
|
8706
|
+
reader.releaseLock();
|
|
8707
|
+
}
|
|
8708
|
+
if (streamError) {
|
|
8709
|
+
throw new Error(streamError);
|
|
8710
|
+
}
|
|
8711
|
+
if (!result) {
|
|
8712
|
+
throw new Error('No command result received');
|
|
8713
|
+
}
|
|
8714
|
+
return result;
|
|
8715
|
+
}
|
|
8716
|
+
async writeFile(path, content, options) {
|
|
8717
|
+
const response = await fetch(`${this.baseUrl}/write-file`, {
|
|
8718
|
+
method: 'POST',
|
|
8719
|
+
headers: this.getHeaders(),
|
|
8720
|
+
credentials: 'include',
|
|
8721
|
+
body: JSON.stringify({
|
|
8722
|
+
path,
|
|
8723
|
+
content,
|
|
8724
|
+
encoding: options?.encoding,
|
|
8725
|
+
overwrite: options?.overwrite,
|
|
8726
|
+
cwd: options?.cwd
|
|
8727
|
+
})
|
|
8728
|
+
});
|
|
8729
|
+
const payload = await response.json().catch(() => ({}));
|
|
8730
|
+
if (!response.ok) {
|
|
8731
|
+
const errorMessage = payload.error || 'Failed to write file';
|
|
8732
|
+
throw new Error(errorMessage);
|
|
8733
|
+
}
|
|
8734
|
+
return payload;
|
|
8735
|
+
}
|
|
8736
|
+
/**
|
|
8737
|
+
* Search workspace for files matching pattern
|
|
8738
|
+
* Executed on Jupyter server using grep/ripgrep
|
|
8739
|
+
*/
|
|
8740
|
+
async searchWorkspace(options) {
|
|
8741
|
+
const response = await fetch(`${this.baseUrl}/search-workspace`, {
|
|
8742
|
+
method: 'POST',
|
|
8743
|
+
headers: this.getHeaders(),
|
|
8744
|
+
credentials: 'include',
|
|
8745
|
+
body: JSON.stringify({
|
|
8746
|
+
pattern: options.pattern,
|
|
8747
|
+
file_types: options.file_types || ['*.py', '*.ipynb'],
|
|
8748
|
+
path: options.path || '.',
|
|
8749
|
+
max_results: options.max_results || 50,
|
|
8750
|
+
case_sensitive: options.case_sensitive || false
|
|
8751
|
+
})
|
|
8752
|
+
});
|
|
8753
|
+
const payload = await response.json().catch(() => ({}));
|
|
8754
|
+
if (!response.ok) {
|
|
8755
|
+
const errorMessage = payload.error || 'Failed to search workspace';
|
|
8756
|
+
throw new Error(errorMessage);
|
|
8757
|
+
}
|
|
8758
|
+
return payload;
|
|
8759
|
+
}
|
|
8760
|
+
/**
|
|
8761
|
+
* Search notebook cells for pattern
|
|
8762
|
+
* Executed on Jupyter server
|
|
8763
|
+
*/
|
|
8764
|
+
async searchNotebookCells(options) {
|
|
8765
|
+
const response = await fetch(`${this.baseUrl}/search-notebook-cells`, {
|
|
8766
|
+
method: 'POST',
|
|
8767
|
+
headers: this.getHeaders(),
|
|
8768
|
+
credentials: 'include',
|
|
8769
|
+
body: JSON.stringify({
|
|
8770
|
+
pattern: options.pattern,
|
|
8771
|
+
notebook_path: options.notebook_path,
|
|
8772
|
+
cell_type: options.cell_type,
|
|
8773
|
+
max_results: options.max_results || 30,
|
|
8774
|
+
case_sensitive: options.case_sensitive || false
|
|
8775
|
+
})
|
|
8776
|
+
});
|
|
8777
|
+
const payload = await response.json().catch(() => ({}));
|
|
8778
|
+
if (!response.ok) {
|
|
8779
|
+
const errorMessage = payload.error || 'Failed to search notebook cells';
|
|
8780
|
+
throw new Error(errorMessage);
|
|
8781
|
+
}
|
|
8782
|
+
return payload;
|
|
8783
|
+
}
|
|
8784
|
+
/**
|
|
8785
|
+
* Check system resources and file sizes before data processing
|
|
8786
|
+
* Executed on Jupyter server
|
|
8787
|
+
*/
|
|
8788
|
+
async checkResource(options) {
|
|
8789
|
+
const response = await fetch(`${this.baseUrl}/check-resource`, {
|
|
8790
|
+
method: 'POST',
|
|
8791
|
+
headers: this.getHeaders(),
|
|
8792
|
+
credentials: 'include',
|
|
8793
|
+
body: JSON.stringify({
|
|
8794
|
+
files: options.files || [],
|
|
8795
|
+
dataframes: options.dataframes || [],
|
|
8796
|
+
file_size_command: options.file_size_command || '',
|
|
8797
|
+
dataframe_check_code: options.dataframe_check_code || ''
|
|
8798
|
+
})
|
|
8799
|
+
});
|
|
8800
|
+
const payload = await response.json().catch(() => ({}));
|
|
8801
|
+
if (!response.ok) {
|
|
8802
|
+
const errorMessage = payload.error || 'Failed to check resources';
|
|
8803
|
+
throw new Error(errorMessage);
|
|
8804
|
+
}
|
|
8805
|
+
return payload;
|
|
8806
|
+
}
|
|
7727
8807
|
/**
|
|
7728
8808
|
* Save configuration
|
|
7729
8809
|
*/
|
|
@@ -9238,9 +10318,9 @@ class ToolExecutor {
|
|
|
9238
10318
|
},
|
|
9239
10319
|
});
|
|
9240
10320
|
}
|
|
9241
|
-
//
|
|
9242
|
-
const executeCommandDef = _ToolRegistry__WEBPACK_IMPORTED_MODULE_1__.BUILTIN_TOOL_DEFINITIONS.find(t => t.name === '
|
|
9243
|
-
if (executeCommandDef && !this.registry.hasTool('
|
|
10321
|
+
// execute_command_tool 도구 등록 (조건부 승인)
|
|
10322
|
+
const executeCommandDef = _ToolRegistry__WEBPACK_IMPORTED_MODULE_1__.BUILTIN_TOOL_DEFINITIONS.find(t => t.name === 'execute_command_tool');
|
|
10323
|
+
if (executeCommandDef && !this.registry.hasTool('execute_command_tool')) {
|
|
9244
10324
|
this.registry.register({
|
|
9245
10325
|
...executeCommandDef,
|
|
9246
10326
|
executor: async (params, context) => {
|
|
@@ -9620,6 +10700,11 @@ class ToolExecutor {
|
|
|
9620
10700
|
isDangerousCommand(command) {
|
|
9621
10701
|
return _ToolRegistry__WEBPACK_IMPORTED_MODULE_1__.DANGEROUS_COMMAND_PATTERNS.some(pattern => pattern.test(command));
|
|
9622
10702
|
}
|
|
10703
|
+
summarizeOutput(output, maxLines = 2) {
|
|
10704
|
+
const lines = output.split(/\r?\n/).filter(line => line.length > 0);
|
|
10705
|
+
const text = lines.slice(0, maxLines).join('\n');
|
|
10706
|
+
return { text, truncated: lines.length > maxLines };
|
|
10707
|
+
}
|
|
9623
10708
|
/**
|
|
9624
10709
|
* read_file 도구: 파일 읽기
|
|
9625
10710
|
*/
|
|
@@ -10037,19 +11122,19 @@ print(json.dumps(result))
|
|
|
10037
11122
|
}
|
|
10038
11123
|
}
|
|
10039
11124
|
/**
|
|
10040
|
-
*
|
|
11125
|
+
* execute_command_tool 도구: 셸 명령 실행 (조건부 승인)
|
|
10041
11126
|
*/
|
|
10042
11127
|
async executeCommand(params, context) {
|
|
10043
11128
|
console.log('[ToolExecutor] executeCommand:', params.command);
|
|
10044
|
-
const timeout = params.timeout
|
|
11129
|
+
const timeout = typeof params.timeout === 'number' ? params.timeout : 600000;
|
|
10045
11130
|
// 위험 명령 검사 및 조건부 승인 요청
|
|
10046
11131
|
if (this.isDangerousCommand(params.command)) {
|
|
10047
11132
|
console.log('[ToolExecutor] Dangerous command detected, requesting approval');
|
|
10048
11133
|
// 승인 요청
|
|
10049
11134
|
const request = {
|
|
10050
|
-
id: `
|
|
10051
|
-
toolName: '
|
|
10052
|
-
toolDefinition: this.registry.getTool('
|
|
11135
|
+
id: `execute_command_tool-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
11136
|
+
toolName: 'execute_command_tool',
|
|
11137
|
+
toolDefinition: this.registry.getTool('execute_command_tool'),
|
|
10053
11138
|
parameters: params,
|
|
10054
11139
|
stepNumber: context.stepNumber,
|
|
10055
11140
|
description: `🔴 위험 명령 실행 요청:\n\n\`${params.command}\`\n\n이 명령은 시스템에 영향을 줄 수 있습니다.`,
|
|
@@ -10066,56 +11151,32 @@ print(json.dumps(result))
|
|
|
10066
11151
|
}
|
|
10067
11152
|
}
|
|
10068
11153
|
}
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
import subprocess
|
|
10073
|
-
import sys
|
|
10074
|
-
try:
|
|
10075
|
-
command = ${JSON.stringify(params.command)}
|
|
10076
|
-
timeout_sec = ${timeout / 1000}
|
|
10077
|
-
|
|
10078
|
-
result = subprocess.run(
|
|
10079
|
-
command,
|
|
10080
|
-
shell=True,
|
|
10081
|
-
capture_output=True,
|
|
10082
|
-
text=True,
|
|
10083
|
-
timeout=timeout_sec
|
|
10084
|
-
)
|
|
10085
|
-
|
|
10086
|
-
output = {
|
|
10087
|
-
'success': result.returncode == 0,
|
|
10088
|
-
'stdout': result.stdout,
|
|
10089
|
-
'stderr': result.stderr,
|
|
10090
|
-
'returncode': result.returncode
|
|
10091
|
-
}
|
|
10092
|
-
except subprocess.TimeoutExpired:
|
|
10093
|
-
output = {'success': False, 'error': f'Command timed out after {timeout_sec}s'}
|
|
10094
|
-
except Exception as e:
|
|
10095
|
-
output = {'success': False, 'error': str(e)}
|
|
10096
|
-
print(json.dumps(output))
|
|
10097
|
-
`.trim();
|
|
11154
|
+
if (!this.apiService) {
|
|
11155
|
+
return { success: false, error: 'ApiService not available for execute_command_tool' };
|
|
11156
|
+
}
|
|
10098
11157
|
try {
|
|
10099
|
-
const
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
success: false,
|
|
10111
|
-
error: parsed.error || parsed.stderr || `Command failed with code ${parsed.returncode}`,
|
|
10112
|
-
};
|
|
10113
|
-
}
|
|
11158
|
+
const result = await this.apiService.executeCommandStream(params.command, { timeout });
|
|
11159
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout : '';
|
|
11160
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : '';
|
|
11161
|
+
const combined = [stdout, stderr].filter(Boolean).join('\n');
|
|
11162
|
+
const summary = this.summarizeOutput(combined, 2);
|
|
11163
|
+
const output = summary.text || '(no output)';
|
|
11164
|
+
if (result.success) {
|
|
11165
|
+
return {
|
|
11166
|
+
success: true,
|
|
11167
|
+
output,
|
|
11168
|
+
};
|
|
10114
11169
|
}
|
|
10115
|
-
|
|
11170
|
+
const errorText = summary.text || result.error || stderr || `Command failed with code ${result.returncode}`;
|
|
11171
|
+
return {
|
|
11172
|
+
success: false,
|
|
11173
|
+
error: errorText,
|
|
11174
|
+
};
|
|
10116
11175
|
}
|
|
10117
11176
|
catch (error) {
|
|
10118
|
-
|
|
11177
|
+
const message = error instanceof Error ? error.message : 'Command execution failed';
|
|
11178
|
+
const summary = this.summarizeOutput(String(message), 2);
|
|
11179
|
+
return { success: false, error: summary.text || 'Command execution failed' };
|
|
10119
11180
|
}
|
|
10120
11181
|
}
|
|
10121
11182
|
/**
|
|
@@ -11901,7 +12962,7 @@ const BUILTIN_TOOL_DEFINITIONS = [
|
|
|
11901
12962
|
// 확장 도구 (시스템)
|
|
11902
12963
|
// ─────────────────────────────────────────────────────────────────────────
|
|
11903
12964
|
{
|
|
11904
|
-
name: '
|
|
12965
|
+
name: 'execute_command_tool',
|
|
11905
12966
|
description: '셸 명령 실행 (위험 명령만 승인 필요)',
|
|
11906
12967
|
riskLevel: 'critical',
|
|
11907
12968
|
requiresApproval: false,
|
|
@@ -11986,7 +13047,7 @@ const BUILTIN_TOOL_DEFINITIONS = [
|
|
|
11986
13047
|
];
|
|
11987
13048
|
/**
|
|
11988
13049
|
* 위험한 셸 명령 패턴들
|
|
11989
|
-
*
|
|
13050
|
+
* execute_command_tool에서 이 패턴에 매칭되는 명령은 사용자 승인 필요
|
|
11990
13051
|
*/
|
|
11991
13052
|
const DANGEROUS_COMMAND_PATTERNS = [
|
|
11992
13053
|
// 파일 삭제/제거
|
|
@@ -12515,6 +13576,154 @@ function normalizeIndentation(code) {
|
|
|
12515
13576
|
});
|
|
12516
13577
|
return normalized.join('\n');
|
|
12517
13578
|
}
|
|
13579
|
+
function extractNextItemsBlock(text) {
|
|
13580
|
+
if (!text.trim())
|
|
13581
|
+
return null;
|
|
13582
|
+
const fencedRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
|
13583
|
+
let match;
|
|
13584
|
+
while ((match = fencedRegex.exec(text)) !== null) {
|
|
13585
|
+
const lang = (match[1] || '').toLowerCase();
|
|
13586
|
+
const content = match[2].trim();
|
|
13587
|
+
if (!content)
|
|
13588
|
+
continue;
|
|
13589
|
+
if (lang && lang !== 'json' && !content.includes('"next_items"')) {
|
|
13590
|
+
continue;
|
|
13591
|
+
}
|
|
13592
|
+
const items = parseNextItemsPayload(content);
|
|
13593
|
+
if (items) {
|
|
13594
|
+
const placeholder = `__NEXT_ITEMS_${Math.random().toString(36).slice(2, 11)}__`;
|
|
13595
|
+
const updated = text.slice(0, match.index) + placeholder + text.slice(match.index + match[0].length);
|
|
13596
|
+
return { items, placeholder, text: updated };
|
|
13597
|
+
}
|
|
13598
|
+
}
|
|
13599
|
+
const range = findNextItemsJsonRange(text);
|
|
13600
|
+
if (!range)
|
|
13601
|
+
return null;
|
|
13602
|
+
const candidate = text.slice(range.start, range.end + 1);
|
|
13603
|
+
const items = parseNextItemsPayload(candidate);
|
|
13604
|
+
if (!items)
|
|
13605
|
+
return null;
|
|
13606
|
+
let replacementStart = range.start;
|
|
13607
|
+
const lineStart = text.lastIndexOf('\n', range.start - 1) + 1;
|
|
13608
|
+
const linePrefix = text.slice(lineStart, range.start).trim();
|
|
13609
|
+
const normalizedPrefix = linePrefix.replace(/[::]$/, '').toLowerCase();
|
|
13610
|
+
if (normalizedPrefix === 'json') {
|
|
13611
|
+
replacementStart = lineStart;
|
|
13612
|
+
}
|
|
13613
|
+
const placeholder = `__NEXT_ITEMS_${Math.random().toString(36).slice(2, 11)}__`;
|
|
13614
|
+
const updated = text.slice(0, replacementStart) + placeholder + text.slice(range.end + 1);
|
|
13615
|
+
return { items, placeholder, text: updated };
|
|
13616
|
+
}
|
|
13617
|
+
function findNextItemsJsonRange(text) {
|
|
13618
|
+
const key = '"next_items"';
|
|
13619
|
+
let searchIndex = text.indexOf(key);
|
|
13620
|
+
while (searchIndex !== -1) {
|
|
13621
|
+
const start = text.lastIndexOf('{', searchIndex);
|
|
13622
|
+
if (start === -1)
|
|
13623
|
+
return null;
|
|
13624
|
+
const end = findMatchingBrace(text, start);
|
|
13625
|
+
if (end !== -1) {
|
|
13626
|
+
return { start, end };
|
|
13627
|
+
}
|
|
13628
|
+
searchIndex = text.indexOf(key, searchIndex + key.length);
|
|
13629
|
+
}
|
|
13630
|
+
return null;
|
|
13631
|
+
}
|
|
13632
|
+
function findMatchingBrace(text, start) {
|
|
13633
|
+
let depth = 0;
|
|
13634
|
+
let inString = false;
|
|
13635
|
+
let escaped = false;
|
|
13636
|
+
for (let i = start; i < text.length; i++) {
|
|
13637
|
+
const char = text[i];
|
|
13638
|
+
if (inString) {
|
|
13639
|
+
if (escaped) {
|
|
13640
|
+
escaped = false;
|
|
13641
|
+
continue;
|
|
13642
|
+
}
|
|
13643
|
+
if (char === '\\') {
|
|
13644
|
+
escaped = true;
|
|
13645
|
+
continue;
|
|
13646
|
+
}
|
|
13647
|
+
if (char === '"') {
|
|
13648
|
+
inString = false;
|
|
13649
|
+
}
|
|
13650
|
+
continue;
|
|
13651
|
+
}
|
|
13652
|
+
if (char === '"') {
|
|
13653
|
+
inString = true;
|
|
13654
|
+
continue;
|
|
13655
|
+
}
|
|
13656
|
+
if (char === '{') {
|
|
13657
|
+
depth += 1;
|
|
13658
|
+
}
|
|
13659
|
+
else if (char === '}') {
|
|
13660
|
+
depth -= 1;
|
|
13661
|
+
if (depth === 0) {
|
|
13662
|
+
return i;
|
|
13663
|
+
}
|
|
13664
|
+
}
|
|
13665
|
+
}
|
|
13666
|
+
return -1;
|
|
13667
|
+
}
|
|
13668
|
+
function parseNextItemsPayload(payload) {
|
|
13669
|
+
if (!payload.startsWith('{') || !payload.endsWith('}'))
|
|
13670
|
+
return null;
|
|
13671
|
+
try {
|
|
13672
|
+
const parsed = JSON.parse(payload);
|
|
13673
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
13674
|
+
return null;
|
|
13675
|
+
const nextItemsRaw = parsed.next_items;
|
|
13676
|
+
if (!Array.isArray(nextItemsRaw))
|
|
13677
|
+
return null;
|
|
13678
|
+
const items = nextItemsRaw
|
|
13679
|
+
.map((item) => {
|
|
13680
|
+
if (!item || typeof item !== 'object')
|
|
13681
|
+
return null;
|
|
13682
|
+
const subject = typeof item.subject === 'string'
|
|
13683
|
+
? item.subject.trim()
|
|
13684
|
+
: '';
|
|
13685
|
+
const description = typeof item.description === 'string'
|
|
13686
|
+
? item.description.trim()
|
|
13687
|
+
: '';
|
|
13688
|
+
if (!subject && !description)
|
|
13689
|
+
return null;
|
|
13690
|
+
return { subject, description };
|
|
13691
|
+
})
|
|
13692
|
+
.filter((item) => Boolean(item));
|
|
13693
|
+
return items.length > 0 ? items : null;
|
|
13694
|
+
}
|
|
13695
|
+
catch {
|
|
13696
|
+
return null;
|
|
13697
|
+
}
|
|
13698
|
+
}
|
|
13699
|
+
function renderNextItemsList(items) {
|
|
13700
|
+
// Simple arrow icon
|
|
13701
|
+
const arrowSvg = `
|
|
13702
|
+
<svg class="jp-next-items-icon" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
13703
|
+
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"/>
|
|
13704
|
+
</svg>`;
|
|
13705
|
+
const listItems = items.map((item) => {
|
|
13706
|
+
const subject = escapeHtml(item.subject);
|
|
13707
|
+
const description = escapeHtml(item.description);
|
|
13708
|
+
const subjectHtml = subject ? `<div class="jp-next-items-subject">${subject}</div>` : '';
|
|
13709
|
+
const descriptionHtml = description ? `<div class="jp-next-items-description">${description}</div>` : '';
|
|
13710
|
+
return `
|
|
13711
|
+
<li class="jp-next-items-item" data-next-item="true" role="button" tabindex="0">
|
|
13712
|
+
<div class="jp-next-items-text">
|
|
13713
|
+
${subjectHtml}
|
|
13714
|
+
${descriptionHtml}
|
|
13715
|
+
</div>
|
|
13716
|
+
${arrowSvg}
|
|
13717
|
+
</li>`;
|
|
13718
|
+
}).join('');
|
|
13719
|
+
return `
|
|
13720
|
+
<div class="jp-next-items" data-next-items="true">
|
|
13721
|
+
<div class="jp-next-items-header">다음 단계 제안</div>
|
|
13722
|
+
<ul class="jp-next-items-list" role="list">
|
|
13723
|
+
${listItems}
|
|
13724
|
+
</ul>
|
|
13725
|
+
</div>`;
|
|
13726
|
+
}
|
|
12518
13727
|
/**
|
|
12519
13728
|
* Highlight Python code with inline styles
|
|
12520
13729
|
*/
|
|
@@ -12915,9 +14124,10 @@ function parseMarkdownTable(tableText) {
|
|
|
12915
14124
|
* Format markdown text to HTML with syntax highlighting
|
|
12916
14125
|
*/
|
|
12917
14126
|
function formatMarkdownToHtml(text) {
|
|
14127
|
+
const nextItemsBlock = extractNextItemsBlock(text);
|
|
12918
14128
|
// Decode HTML entities if present
|
|
12919
14129
|
const textarea = document.createElement('textarea');
|
|
12920
|
-
textarea.innerHTML = text;
|
|
14130
|
+
textarea.innerHTML = nextItemsBlock ? nextItemsBlock.text : text;
|
|
12921
14131
|
let html = textarea.value;
|
|
12922
14132
|
// Step 0.5: Protect DataFrame HTML tables (must be before code blocks)
|
|
12923
14133
|
const dataframeHtmlPlaceholders = [];
|
|
@@ -12959,6 +14169,9 @@ function formatMarkdownToHtml(text) {
|
|
|
12959
14169
|
'</div>' +
|
|
12960
14170
|
'</div>' +
|
|
12961
14171
|
'<pre class="code-block language-' + escapeHtml(lang) + '"><code id="' + blockId + '">' + highlightedCode + '</code></pre>' +
|
|
14172
|
+
'<button class="code-block-toggle" data-block-id="' + blockId + '" title="전체 보기" aria-label="전체 보기" aria-expanded="false">' +
|
|
14173
|
+
'<span class="code-block-toggle-icon" aria-hidden="true">▾</span>' +
|
|
14174
|
+
'</button>' +
|
|
12962
14175
|
'</div>';
|
|
12963
14176
|
codeBlockPlaceholders.push({
|
|
12964
14177
|
placeholder: placeholder,
|
|
@@ -13020,7 +14233,7 @@ function formatMarkdownToHtml(text) {
|
|
|
13020
14233
|
return '\n' + placeholder + '\n';
|
|
13021
14234
|
});
|
|
13022
14235
|
// Step 4: Escape HTML for non-placeholder text
|
|
13023
|
-
html = html.split(/(__(?:DATAFRAME_HTML|CODE_BLOCK|INLINE_CODE|TABLE)_[a-z0-9-]+__)/gi)
|
|
14236
|
+
html = html.split(/(__(?:DATAFRAME_HTML|CODE_BLOCK|INLINE_CODE|TABLE|NEXT_ITEMS)_[a-z0-9-]+__)/gi)
|
|
13024
14237
|
.map((part, index) => {
|
|
13025
14238
|
// Odd indices are placeholders - keep as is
|
|
13026
14239
|
if (index % 2 === 1)
|
|
@@ -13068,6 +14281,10 @@ function formatMarkdownToHtml(text) {
|
|
|
13068
14281
|
codeBlockPlaceholders.forEach(item => {
|
|
13069
14282
|
html = html.replace(item.placeholder, item.html);
|
|
13070
14283
|
});
|
|
14284
|
+
// Step 8.5: Restore next items list placeholders
|
|
14285
|
+
if (nextItemsBlock) {
|
|
14286
|
+
html = html.split(nextItemsBlock.placeholder).join(renderNextItemsList(nextItemsBlock.items));
|
|
14287
|
+
}
|
|
13071
14288
|
return html;
|
|
13072
14289
|
}
|
|
13073
14290
|
|
|
@@ -13229,4 +14446,4 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
13229
14446
|
/***/ }
|
|
13230
14447
|
|
|
13231
14448
|
}]);
|
|
13232
|
-
//# sourceMappingURL=lib_index_js.
|
|
14449
|
+
//# sourceMappingURL=lib_index_js.29cf4312af19e86f82af.js.map
|