hdsp-jupyter-extension 2.0.6__py3-none-any.whl → 2.0.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. agent_server/core/embedding_service.py +67 -46
  2. agent_server/core/rag_manager.py +31 -17
  3. agent_server/core/reflection_engine.py +0 -1
  4. agent_server/core/retriever.py +13 -8
  5. agent_server/core/vllm_embedding_service.py +243 -0
  6. agent_server/knowledge/watchdog_service.py +1 -1
  7. agent_server/langchain/ARCHITECTURE.md +1193 -0
  8. agent_server/langchain/agent.py +82 -588
  9. agent_server/langchain/custom_middleware.py +663 -0
  10. agent_server/langchain/executors/__init__.py +2 -7
  11. agent_server/langchain/executors/notebook_searcher.py +46 -38
  12. agent_server/langchain/hitl_config.py +71 -0
  13. agent_server/langchain/llm_factory.py +166 -0
  14. agent_server/langchain/logging_utils.py +223 -0
  15. agent_server/langchain/prompts.py +150 -0
  16. agent_server/langchain/state.py +16 -6
  17. agent_server/langchain/tools/__init__.py +19 -0
  18. agent_server/langchain/tools/file_tools.py +354 -114
  19. agent_server/langchain/tools/file_utils.py +334 -0
  20. agent_server/langchain/tools/jupyter_tools.py +18 -18
  21. agent_server/langchain/tools/lsp_tools.py +264 -0
  22. agent_server/langchain/tools/resource_tools.py +161 -0
  23. agent_server/langchain/tools/search_tools.py +198 -216
  24. agent_server/langchain/tools/shell_tools.py +54 -0
  25. agent_server/main.py +11 -1
  26. agent_server/routers/health.py +1 -1
  27. agent_server/routers/langchain_agent.py +1040 -289
  28. agent_server/routers/rag.py +8 -3
  29. hdsp_agent_core/models/rag.py +15 -1
  30. hdsp_agent_core/prompts/auto_agent_prompts.py +3 -3
  31. hdsp_agent_core/services/rag_service.py +6 -1
  32. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
  33. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +3 -2
  34. 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.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js +470 -7
  35. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js.map +1 -0
  36. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js +3196 -441
  37. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +1 -0
  38. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.addf2fa038fa60304aa2.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js +9 -7
  39. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +1 -0
  40. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/METADATA +2 -1
  41. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/RECORD +75 -69
  42. jupyter_ext/__init__.py +18 -0
  43. jupyter_ext/_version.py +1 -1
  44. jupyter_ext/handlers.py +1351 -58
  45. jupyter_ext/labextension/build_log.json +1 -1
  46. jupyter_ext/labextension/package.json +3 -2
  47. jupyter_ext/labextension/static/{frontend_styles_index_js.02d346171474a0fb2dc1.js → frontend_styles_index_js.8740a527757068814573.js} +470 -7
  48. jupyter_ext/labextension/static/frontend_styles_index_js.8740a527757068814573.js.map +1 -0
  49. jupyter_ext/labextension/static/{lib_index_js.a223ea20056954479ae9.js → lib_index_js.e4ff4b5779b5e049f84c.js} +3196 -441
  50. jupyter_ext/labextension/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +1 -0
  51. jupyter_ext/labextension/static/{remoteEntry.addf2fa038fa60304aa2.js → remoteEntry.020cdb0b864cfaa4e41e.js} +9 -7
  52. jupyter_ext/labextension/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +1 -0
  53. jupyter_ext/resource_usage.py +180 -0
  54. jupyter_ext/tests/test_handlers.py +58 -0
  55. agent_server/langchain/executors/jupyter_executor.py +0 -429
  56. agent_server/langchain/middleware/__init__.py +0 -36
  57. agent_server/langchain/middleware/code_search_middleware.py +0 -278
  58. agent_server/langchain/middleware/error_handling_middleware.py +0 -338
  59. agent_server/langchain/middleware/jupyter_execution_middleware.py +0 -301
  60. agent_server/langchain/middleware/rag_middleware.py +0 -227
  61. agent_server/langchain/middleware/validation_middleware.py +0 -240
  62. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.02d346171474a0fb2dc1.js.map +0 -1
  63. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
  64. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.addf2fa038fa60304aa2.js.map +0 -1
  65. jupyter_ext/labextension/static/frontend_styles_index_js.02d346171474a0fb2dc1.js.map +0 -1
  66. jupyter_ext/labextension/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
  67. jupyter_ext/labextension/static/remoteEntry.addf2fa038fa60304aa2.js.map +0 -1
  68. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
  69. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  70. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js +0 -0
  71. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js.map +0 -0
  72. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js +0 -0
  73. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js.map +0 -0
  74. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
  75. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js +0 -0
  76. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js.map +0 -0
  77. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +0 -0
  78. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +0 -0
  79. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +0 -0
  80. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +0 -0
  81. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
  82. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js.map +0 -0
  83. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
  84. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
  85. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +0 -0
  86. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -0
  87. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/WHEEL +0 -0
  88. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1193 @@
1
+ # LangChain Agent Architecture
2
+
3
+ ## 목차
4
+ 1. [개요](#개요)
5
+ 2. [전체 구조](#전체-구조)
6
+ 3. [디렉토리 구조](#디렉토리-구조)
7
+ 4. [핵심 컴포넌트](#핵심-컴포넌트)
8
+ 5. [데이터 흐름](#데이터-흐름)
9
+ 6. [미들웨어 시스템](#미들웨어-시스템)
10
+ 7. [도구 시스템](#도구-시스템)
11
+ 8. [실행 흐름](#실행-흐름)
12
+
13
+ ---
14
+
15
+ ## 개요
16
+
17
+ 이 시스템은 LangChain을 기반으로 Jupyter 노트북 환경에서 동작하는 데이터 분석 에이전트입니다. 주요 특징:
18
+
19
+ - **Human-in-the-Loop (HITL)**: 코드 실행 전 사용자 승인 필요
20
+ - **TodoList 기반 작업 관리**: 작업을 단계별로 추적
21
+ - **스트리밍 응답**: 실시간으로 에이전트 상태 전달
22
+ - **멀티 모델 지원**: Gemini, OpenAI, vLLM
23
+ - **커스텀 미들웨어**: 빈 응답 처리, continuation 프롬프트 주입 등
24
+
25
+ ---
26
+
27
+ ## 전체 구조
28
+
29
+ ```
30
+ ┌─────────────────────────────────────────────────────────────┐
31
+ │ Jupyter Extension (Frontend) │
32
+ │ - AgentPanel.tsx: UI 컴포넌트 │
33
+ │ - ApiService.ts: API 통신 │
34
+ └──────────────────────┬──────────────────────────────────────┘
35
+ │ HTTP/SSE
36
+ ┌──────────────────────▼──────────────────────────────────────┐
37
+ │ Jupyter Extension (Backend) │
38
+ │ - handlers.py: HTTP 핸들러 │
39
+ │ - ChatStreamHandler: 에이전트 스트리밍 │
40
+ │ - ExecuteCommandHandler: 쉘 명령 실행 │
41
+ │ - CheckResourceHandler: 리소스 확인 │
42
+ └──────────────────────┬──────────────────────────────────────┘
43
+ │ HTTP POST
44
+ ┌──────────────────────▼──────────────────────────────────────┐
45
+ │ Agent Server (FastAPI) │
46
+ │ - langchain_agent.py: 라우터 │
47
+ │ - stream_agent(): 초기 요청 처리 │
48
+ │ - resume_agent(): 인터럽트 재개 │
49
+ └──────────────────────┬──────────────────────────────────────┘
50
+
51
+ ┌──────────────────────▼──────────────────────────────────────┐
52
+ │ LangChain Agent (agent.py) │
53
+ │ - create_simple_chat_agent(): 에이전트 생성 │
54
+ │ - Middleware 체인 │
55
+ │ - Tools 등록 │
56
+ └──────────────────────┬──────────────────────────────────────┘
57
+
58
+ ┌─────────────┼─────────────┐
59
+ │ │ │
60
+ ┌────────▼────────┐ ┌─▼──────────┐ ┌▼─────────────┐
61
+ │ Middleware │ │ Tools │ │ LLM Factory │
62
+ │ │ │ │ │ │
63
+ │ - Empty Response│ │ - Jupyter │ │ - Gemini │
64
+ │ - Continuation │ │ - File I/O │ │ - OpenAI │
65
+ │ - HITL │ │ - Search │ │ - vLLM │
66
+ │ - TodoList │ │ - Shell │ │ │
67
+ └─────────────────┘ └────────────┘ └──────────────┘
68
+ ```
69
+
70
+ ---
71
+
72
+ ## 디렉토리 구조
73
+
74
+ ### `agent_server/langchain/`
75
+
76
+ ```
77
+ langchain/
78
+ ├── __init__.py # 모듈 초기화
79
+ ├── agent.py # 에이전트 생성 및 설정
80
+ ├── custom_middleware.py # 커스텀 미들웨어 정의
81
+ ├── hitl_config.py # HITL 설정
82
+ ├── llm_factory.py # LLM 인스턴스 생성
83
+ ├── logging_utils.py # 로깅 유틸리티
84
+ ├── prompts.py # 시스템 프롬프트 및 스키마
85
+ ├── state.py # 상태 정의 (TypedDict, dataclass)
86
+ ├── executors/
87
+ │ ├── __init__.py
88
+ │ └── notebook_searcher.py # 노트북 검색 기능
89
+ └── tools/
90
+ ├── __init__.py
91
+ ├── file_tools.py # 파일 읽기/쓰기/목록
92
+ ├── jupyter_tools.py # Jupyter 셀 실행, 마크다운, final_answer
93
+ ├── resource_tools.py # 리소스 확인 (파일 크기, 메모리)
94
+ ├── search_tools.py # 워크스페이스 검색, 노트북 검색
95
+ └── shell_tools.py # 쉘 명령 실행
96
+ ```
97
+
98
+ ### `agent_server/routers/`
99
+
100
+ ```
101
+ routers/
102
+ └── langchain_agent.py # FastAPI 라우터
103
+ - stream_agent() # POST /agent/langchain/stream
104
+ - resume_agent() # POST /agent/langchain/resume
105
+ - search_workspace() # POST /agent/search/workspace
106
+ - clear_agent_cache() # POST /agent/langchain/clear
107
+ ```
108
+
109
+ ### `extensions/jupyter/jupyter_ext/`
110
+
111
+ ```
112
+ jupyter_ext/
113
+ └── handlers.py # Jupyter Extension 핸들러
114
+ - ChatStreamHandler # GET /hdsp-agent/chat/stream
115
+ - ExecuteCommandHandler # POST /hdsp-agent/execute/command
116
+ - CheckResourceHandler # POST /hdsp-agent/check-resource
117
+ - WriteFileHandler # POST /hdsp-agent/write-file
118
+ - (기타 핸들러)
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 핵심 컴포넌트
124
+
125
+ ### 1. Agent (`agent.py`)
126
+
127
+ #### `create_simple_chat_agent()`
128
+ 에이전트 인스턴스를 생성하고 설정합니다.
129
+
130
+ **주요 작업**:
131
+ 1. LLM 생성 (`llm_factory.create_llm()`)
132
+ 2. Tools 등록 (`_get_all_tools()`)
133
+ 3. Middleware 체인 구성
134
+ 4. Checkpointer 설정 (InMemorySaver)
135
+ 5. 시스템 프롬프트 설정 (Gemini-2.5-flash 전용 프롬프트 추가)
136
+
137
+ **Middleware 순서**:
138
+ ```python
139
+ 1. handle_empty_response # 빈 응답 처리
140
+ 2. limit_tool_calls # 한 번에 1개 도구만 호출
141
+ 3. inject_continuation # non-HITL 도구 후 continuation 프롬프트
142
+ 4. patch_tool_calls # dangling tool call 수정
143
+ 5. TodoListMiddleware # 작업 목록 관리
144
+ 6. HumanInTheLoopMiddleware # 코드 실행 전 사용자 승인
145
+ 7. ModelCallLimitMiddleware # LLM 호출 횟수 제한 (30회)
146
+ 8. ToolCallLimitMiddleware # 특정 도구 호출 제한
147
+ 9. SummarizationMiddleware # 대화 요약
148
+ ```
149
+
150
+ **Tools**:
151
+ ```python
152
+ - jupyter_cell_tool # Python 코드 실행
153
+ - markdown_tool # 마크다운 셀 추가
154
+ - final_answer_tool # 작업 완료 및 요약
155
+ - read_file_tool # 파일 읽기
156
+ - write_file_tool # 파일 쓰기
157
+ - list_files_tool # 디렉토리 목록
158
+ - search_workspace_tool # 워크스페이스 검색 (grep/rg)
159
+ - search_notebook_cells_tool # 노트북 셀 검색
160
+ - execute_command_tool # 쉘 명령 실행
161
+ - check_resource_tool # 리소스 확인
162
+ ```
163
+
164
+ ---
165
+
166
+ ### 2. Router (`langchain_agent.py`)
167
+
168
+ FastAPI 라우터로, 클라이언트 요청을 받아 에이전트를 실행합니다.
169
+
170
+ #### `stream_agent()` - POST `/agent/langchain/stream`
171
+ 초기 요청을 처리하고 SSE 스트리밍으로 응답합니다.
172
+
173
+ **흐름**:
174
+ ```
175
+ 1. AgentRequest 파싱
176
+ 2. 설정 준비 (LLM config, workspace root, thread_id)
177
+ 3. 에이전트 생성 (create_simple_chat_agent)
178
+ 4. Checkpointer 생성/조회 (InMemorySaver)
179
+ 5. 스트리밍 시작 (agent.stream)
180
+ 6. 이벤트 처리 루프:
181
+ - todos: TodoList 업데이트
182
+ - messages: AIMessage/ToolMessage 처리
183
+ - interrupt: HITL 인터럽트 발생
184
+ 7. SSE 이벤트 전송:
185
+ - event: todos # Todo 리스트 업데이트
186
+ - event: token # LLM 응답 토큰
187
+ - event: debug # 디버그 메시지
188
+ - event: tool_call # 도구 호출 요청
189
+ - event: interrupt # HITL 인터럽트
190
+ - event: complete # 완료
191
+ ```
192
+
193
+ **주요 처리**:
194
+ - **ToolMessage (final_answer_tool)**: `final_answer` 추출, `summary` 필드에서 `next_items` JSON 추출 후 마크다운 코드 블록으로 변환
195
+ - **AIMessage**: tool_calls 확인, 빈 content는 필터링, 중복 제거
196
+ - **Interrupt**: HITL 도구 호출 시 스트리밍 일시 중지, 클라이언트로 interrupt 이벤트 전송
197
+
198
+ #### `resume_agent()` - POST `/agent/langchain/resume`
199
+ HITL 인터럽트 이후 사용자 결정(승인/거부)을 받아 에이전트를 재개합니다.
200
+
201
+ **흐름**:
202
+ ```
203
+ 1. ResumeRequest 파싱 (thread_id, decision, execution_result)
204
+ 2. Checkpointer에서 기존 상태 조회
205
+ 3. 인터럽트 메시지 찾기
206
+ 4. 사용자 결정에 따라 업데이트:
207
+ - approved: execution_result를 ToolMessage arguments에 주입
208
+ - rejected: rejection_reason 추가
209
+ 5. 에이전트 재개 (agent.stream)
210
+ 6. SSE 이벤트 전송 (stream_agent와 동일)
211
+ ```
212
+
213
+ ---
214
+
215
+ ### 3. Handlers (`handlers.py`)
216
+
217
+ Jupyter Extension의 백엔드 핸들러로, 클라이언트 요청을 Agent Server로 전달합니다.
218
+
219
+ #### `ChatStreamHandler` - GET `/hdsp-agent/chat/stream`
220
+ 에이전트와의 대화를 스트리밍합니다.
221
+
222
+ **흐름**:
223
+ ```
224
+ 1. GET 파라미터 파싱 (message, sessionId, mode 등)
225
+ 2. Agent Server로 POST 요청
226
+ - URL: {AGENT_SERVER_URL}/agent/langchain/stream
227
+ - Body: AgentRequest
228
+ 3. SSE 스트리밍 응답 전달
229
+ 4. event: tool_call 감지 시:
230
+ - jupyter_cell_tool: 클라이언트에 전달 (HITL)
231
+ - execute_command_tool: 서버에서 실행 후 결과 반환
232
+ - check_resource_tool: 서버에서 실행 후 결과 반환
233
+ 5. interrupt 이벤트 수신 시: 클라이언트로 전달, 대기
234
+ ```
235
+
236
+ #### `ExecuteCommandHandler` - POST `/hdsp-agent/execute/command`
237
+ 쉘 명령을 실행하고 결과를 반환합니다.
238
+
239
+ **흐름**:
240
+ ```
241
+ 1. POST body 파싱 (command, stdin, cwd, timeout)
242
+ 2. subprocess로 명령 실행
243
+ 3. stdout/stderr 수집
244
+ 4. 결과 반환 (success, output, error)
245
+ ```
246
+
247
+ #### `CheckResourceHandler` - POST `/hdsp-agent/check-resource`
248
+ 파일 크기 및 DataFrame 메모리 사용량을 확인합니다.
249
+
250
+ **흐름**:
251
+ ```
252
+ 1. POST body 파싱 (files, dataframes)
253
+ 2. 파일 크기 확인 (subprocess: du -sh)
254
+ 3. DataFrame 메모리 확인 (jupyter_cell_tool 호출)
255
+ 4. 결과 반환 (file_sizes, dataframe_memory)
256
+ ```
257
+
258
+ ---
259
+
260
+ ## 데이터 흐름
261
+
262
+ ### 초기 요청 흐름
263
+
264
+ ```
265
+ [Client]
266
+
267
+ ├─ message: "타이타닉 데이터 분석해줘"
268
+
269
+
270
+ [Jupyter Extension: ChatStreamHandler]
271
+
272
+ ├─ POST /agent/langchain/stream
273
+ │ {
274
+ │ request: "타이타닉 데이터 분석해줘",
275
+ │ threadId: "uuid",
276
+ │ workspaceRoot: "/path/to/workspace",
277
+ │ llmConfig: { provider: "gemini", ... }
278
+ │ }
279
+
280
+
281
+ [Agent Server: stream_agent()]
282
+
283
+ ├─ create_simple_chat_agent(llm_config, workspace_root)
284
+ │ │
285
+ │ ├─ create_llm() → ChatGoogleGenerativeAI
286
+ │ ├─ _get_all_tools() → [jupyter_cell_tool, ...]
287
+ │ ├─ middleware 체인 구성
288
+ │ └─ create_agent(model, tools, middleware, checkpointer)
289
+
290
+ ├─ agent.stream(input, config)
291
+ │ │
292
+ │ ├─ [TodoListMiddleware] → write_todos 호출
293
+ │ │ → todos: [
294
+ │ │ {content: "데이터 로드", status: "pending"},
295
+ │ │ {content: "EDA", status: "pending"},
296
+ │ │ {content: "다음 단계 제시", status: "pending"}
297
+ │ │ ]
298
+ │ │
299
+ │ ├─ [LLM] → AIMessage with tool_calls
300
+ │ │ → tool_calls: [{
301
+ │ │ name: "check_resource_tool",
302
+ │ │ args: {files: ["titanic.csv"]}
303
+ │ │ }]
304
+ │ │
305
+ │ ├─ [HumanInTheLoopMiddleware] → interrupt (non-HITL이면 통과)
306
+ │ │
307
+ │ ├─ [Tool Execution] → check_resource_tool()
308
+ │ │ → {status: "pending_execution", ...}
309
+ │ │
310
+ │ └─ [Stream] → SSE 이벤트 전송
311
+ │ - event: todos
312
+ │ - event: debug (🔧 Tool 실행: check_resource_tool)
313
+ │ - event: tool_call
314
+
315
+
316
+ [Jupyter Extension: ChatStreamHandler]
317
+
318
+ ├─ tool_call 수신 (check_resource_tool)
319
+ │ → CheckResourceHandler 호출
320
+ │ → execution_result 획득
321
+
322
+ ├─ POST /agent/langchain/resume
323
+ │ {
324
+ │ threadId: "uuid",
325
+ │ decision: "approved",
326
+ │ execution_result: {...}
327
+ │ }
328
+
329
+
330
+ [Agent Server: resume_agent()]
331
+
332
+ ├─ 인터럽트 메시지 업데이트 (execution_result 주입)
333
+
334
+ ├─ agent.stream(None, config) → 재개
335
+ │ │
336
+ │ ├─ [LLM] → ToolMessage 처리
337
+ │ │ → "파일 크기: 60KB"
338
+ │ │
339
+ │ ├─ [inject_continuation] → continuation 프롬프트 주입
340
+ │ │ → "[SYSTEM] Tool 'check_resource_tool' completed. Continue..."
341
+ │ │
342
+ │ ├─ [LLM] → AIMessage with tool_calls
343
+ │ │ → tool_calls: [{
344
+ │ │ name: "jupyter_cell_tool",
345
+ │ │ args: {code: "import pandas as pd\ndf = pd.read_csv('titanic.csv')"}
346
+ │ │ }]
347
+ │ │
348
+ │ ├─ [HumanInTheLoopMiddleware] → interrupt (HITL)
349
+ │ │
350
+ │ └─ [Stream] → SSE 이벤트 전송
351
+ │ - event: interrupt
352
+
353
+
354
+ [Jupyter Extension: ChatStreamHandler]
355
+
356
+ ├─ interrupt 수신 (jupyter_cell_tool)
357
+ │ → 클라이언트로 전달 (UI에서 사용자 승인 대기)
358
+
359
+ ├─ 사용자 승인 후
360
+ │ → Jupyter 커널에서 코드 실행
361
+ │ → execution_result 획득
362
+
363
+ ├─ POST /agent/langchain/resume
364
+
365
+
366
+ ... (반복)
367
+ ```
368
+
369
+ ### final_answer_tool 처리 흐름
370
+
371
+ ```
372
+ [LLM]
373
+
374
+ ├─ AIMessage with tool_calls
375
+ │ → tool_calls: [{
376
+ │ name: "final_answer_tool",
377
+ │ args: {
378
+ │ answer: "분석 완료",
379
+ │ summary: '{"next_items": [...]}' // JSON 문자열
380
+ │ }
381
+ │ }]
382
+
383
+
384
+ [Tool Execution]
385
+
386
+ ├─ final_answer_tool(answer, summary)
387
+ │ → {
388
+ │ tool: "final_answer",
389
+ │ parameters: {answer: "...", summary: "..."},
390
+ │ status: "complete"
391
+ │ }
392
+
393
+
394
+ [Router: stream_agent()]
395
+
396
+ ├─ ToolMessage 수신
397
+ │ │
398
+ │ ├─ tool_result.get("answer")
399
+ │ ├─ summary = tool_result.get("summary")
400
+ │ │
401
+ │ ├─ summary가 JSON 문자열이면:
402
+ │ │ │
403
+ │ │ ├─ summary_json = json.loads(summary)
404
+ │ │ ├─ if "next_items" in summary_json:
405
+ │ │ │ next_items_block = f"\n\n```json\n{json.dumps(summary_json)}\n```"
406
+ │ │ │ final_answer = answer + next_items_block
407
+ │ │ │
408
+ │ │ └─ yield {"event": "token", "data": {"content": final_answer}}
409
+ │ │
410
+ │ ├─ yield {"event": "todos", "data": {"todos": _complete_todos(todos)}}
411
+ │ ├─ yield {"event": "debug_clear"}
412
+ │ └─ yield {"event": "complete"}
413
+
414
+ └─ return (스트림 종료)
415
+ ```
416
+
417
+ ---
418
+
419
+ ## 미들웨어 시스템
420
+
421
+ ### 1. `handle_empty_response`
422
+ 빈 응답 또는 text-only 응답을 처리합니다.
423
+
424
+ **동작**:
425
+ 1. LLM 응답 확인:
426
+ - `tool_calls` 있으면 → 정상 응답, 통과
427
+ - `content`에 JSON이 있으면 → 파싱하여 tool_call 생성
428
+ 2. 마지막 메시지가 `final_answer_tool` 결과이면 → 그대로 반환 (에이전트 자연 종료)
429
+ 3. 빈 응답이면 → JSON 스키마 프롬프트로 재시도 (최대 2회)
430
+ 4. 재시도 실패 시 → synthetic `final_answer_tool` 생성
431
+
432
+ **Gemini 2.5 Flash 대응**:
433
+ - content가 리스트인 경우 처리 (`parse_json_tool_call`)
434
+ - multimodal 응답 지원
435
+
436
+ ### 2. `inject_continuation`
437
+ non-HITL 도구 실행 후 continuation 프롬프트를 주입합니다.
438
+
439
+ **대상 도구**:
440
+ ```python
441
+ NON_HITL_TOOLS = {
442
+ "markdown_tool",
443
+ "read_file_tool",
444
+ "list_files_tool",
445
+ "search_workspace_tool",
446
+ "search_notebook_cells_tool",
447
+ "write_todos",
448
+ }
449
+ ```
450
+
451
+ **동작**:
452
+ 1. 마지막 메시지가 non-HITL 도구의 ToolMessage인지 확인
453
+ 2. todos 상태 확인:
454
+ - pending/in_progress 있으면 → "Continue with pending tasks: ..."
455
+ - 모두 완료이면 → "Call final_answer_tool with a summary NOW."
456
+ 3. HumanMessage로 프롬프트 주입
457
+
458
+ ### 3. `limit_tool_calls`
459
+ 한 번에 1개 도구만 호출하도록 제한합니다.
460
+
461
+ **동작**:
462
+ 1. AIMessage의 `tool_calls` 개수 확인
463
+ 2. 2개 이상이면 → 첫 번째만 유지, 나머지 제거
464
+ 3. 로그 출력
465
+
466
+ ### 4. `patch_tool_calls`
467
+ Dangling tool call (실행되지 않은 도구 호출)을 수정합니다.
468
+
469
+ **동작**:
470
+ 1. 마지막 메시지가 AIMessage with tool_calls인지 확인
471
+ 2. 그 다음 메시지가 ToolMessage가 아니면 → dangling
472
+ 3. synthetic ToolMessage 생성하여 주입
473
+
474
+ ### 5. `TodoListMiddleware` (LangChain 내장)
475
+ 작업 목록을 관리합니다.
476
+
477
+ **동작**:
478
+ 1. `write_todos` 도구 등록
479
+ 2. LLM이 `write_todos` 호출하면 → state에 todos 저장
480
+ 3. 시스템 프롬프트에 todo 관리 지침 추가
481
+
482
+ ### 6. `HumanInTheLoopMiddleware` (LangChain 내장)
483
+ 사용자 승인이 필요한 도구 실행 전 인터럽트를 발생시킵니다.
484
+
485
+ **대상 도구**:
486
+ ```python
487
+ HITL_TOOLS = {
488
+ "jupyter_cell_tool",
489
+ "execute_command_tool",
490
+ "write_file_tool",
491
+ }
492
+ ```
493
+
494
+ **동작**:
495
+ 1. AIMessage with tool_calls 감지
496
+ 2. tool_calls 중 HITL 도구가 있으면 → interrupt 발생
497
+ 3. 에이전트 일시 중지, 클라이언트로 제어 반환
498
+
499
+ ### 7. `ModelCallLimitMiddleware` (LangChain 내장)
500
+ LLM 호출 횟수를 제한합니다.
501
+
502
+ **설정**:
503
+ - `run_limit=30`: 최대 30회 LLM 호출
504
+ - `exit_behavior="end"`: 제한 도달 시 에이전트 종료
505
+
506
+ ### 8. `ToolCallLimitMiddleware` (LangChain 내장)
507
+ 특정 도구의 호출 횟수를 제한합니다.
508
+
509
+ **설정**:
510
+ ```python
511
+ - write_todos: run_limit=5, exit_behavior="continue"
512
+ - list_files_tool: run_limit=5, exit_behavior="continue"
513
+ ```
514
+
515
+ ### 9. `SummarizationMiddleware` (LangChain 내장)
516
+ 대화가 길어지면 요약합니다.
517
+
518
+ **설정**:
519
+ - `trigger`: tokens=8000 또는 messages=30
520
+ - `keep`: 최근 10개 메시지 유지
521
+ - `summary_prefix`: "[이전 대화 요약]\n"
522
+
523
+ ---
524
+
525
+ ## 도구 시스템
526
+
527
+ ### Jupyter Tools (`jupyter_tools.py`)
528
+
529
+ #### `jupyter_cell_tool`
530
+ Python 코드를 Jupyter 셀에서 실행합니다.
531
+
532
+ **파라미터**:
533
+ - `code`: Python 코드
534
+ - `description`: 코드 설명 (선택)
535
+ - `execution_result`: 클라이언트에서 실행한 결과 (HITL 후)
536
+
537
+ **반환**:
538
+ ```python
539
+ {
540
+ "tool": "jupyter_cell",
541
+ "parameters": {"code": "...", "description": "..."},
542
+ "status": "pending_execution", # 또는 "complete"
543
+ "message": "Code cell queued for execution...",
544
+ "execution_result": {...} # HITL 후
545
+ }
546
+ ```
547
+
548
+ **특징**:
549
+ - 마크다운 코드 블록 래퍼 제거
550
+ - HITL 대상 (사용자 승인 필요)
551
+
552
+ #### `markdown_tool`
553
+ 마크다운 셀을 추가합니다.
554
+
555
+ **파라미터**:
556
+ - `content`: 마크다운 내용
557
+
558
+ **반환**:
559
+ ```python
560
+ {
561
+ "tool": "markdown",
562
+ "parameters": {"content": "..."},
563
+ "status": "completed",
564
+ "message": "Markdown cell added successfully."
565
+ }
566
+ ```
567
+
568
+ **특징**:
569
+ - non-HITL (즉시 실행)
570
+
571
+ #### `final_answer_tool`
572
+ 작업을 완료하고 요약을 제공합니다.
573
+
574
+ **파라미터**:
575
+ - `answer`: 최종 답변
576
+ - `summary`: 요약 (선택, `next_items` JSON 포함 가능)
577
+
578
+ **반환**:
579
+ ```python
580
+ {
581
+ "tool": "final_answer",
582
+ "parameters": {"answer": "...", "summary": "..."},
583
+ "status": "complete",
584
+ "message": "Task completed successfully"
585
+ }
586
+ ```
587
+
588
+ **특징**:
589
+ - 에이전트 종료 신호
590
+ - `summary` 필드에 `next_items` JSON 포함 가능 (Gemini)
591
+
592
+ ---
593
+
594
+ ### File Tools (`file_tools.py`)
595
+
596
+ #### `read_file_tool`
597
+ 파일을 읽습니다.
598
+
599
+ **파라미터**:
600
+ - `path`: 파일 경로
601
+
602
+ **반환**:
603
+ ```python
604
+ {
605
+ "tool": "read_file",
606
+ "parameters": {"path": "..."},
607
+ "status": "completed",
608
+ "content": "파일 내용..."
609
+ }
610
+ ```
611
+
612
+ **특징**:
613
+ - workspace_root 기준 상대 경로
614
+ - 경로 벗어나기 방지 (`_validate_path`)
615
+
616
+ #### `write_file_tool`
617
+ 파일을 씁니다.
618
+
619
+ **파라미터**:
620
+ - `path`: 파일 경로
621
+ - `content`: 내용
622
+ - `overwrite`: 덮어쓰기 여부 (기본 False)
623
+
624
+ **반환**:
625
+ ```python
626
+ {
627
+ "tool": "write_file",
628
+ "parameters": {"path": "...", "content": "...", "overwrite": False},
629
+ "status": "pending_execution", # HITL
630
+ "message": "File write queued..."
631
+ }
632
+ ```
633
+
634
+ **특징**:
635
+ - HITL 대상 (사용자 승인 필요)
636
+
637
+ #### `list_files_tool`
638
+ 디렉토리 목록을 가져옵니다.
639
+
640
+ **파라미터**:
641
+ - `path`: 디렉토리 경로 (기본 ".")
642
+ - `recursive`: 재귀 탐색 여부 (기본 False)
643
+
644
+ **반환**:
645
+ ```python
646
+ {
647
+ "tool": "list_files",
648
+ "parameters": {"path": ".", "recursive": False},
649
+ "status": "completed",
650
+ "files": ["file1.py", "file2.csv", ...]
651
+ }
652
+ ```
653
+
654
+ ---
655
+
656
+ ### Search Tools (`search_tools.py`)
657
+
658
+ #### `search_workspace_tool`
659
+ 워크스페이스에서 패턴을 검색합니다 (grep/ripgrep).
660
+
661
+ **파라미터**:
662
+ - `pattern`: 정규식 패턴
663
+ - `file_types`: 파일 타입 필터 (예: ["py", "md"])
664
+ - `path`: 검색 경로 (기본 ".")
665
+
666
+ **반환**:
667
+ ```python
668
+ {
669
+ "tool": "search_workspace",
670
+ "parameters": {"pattern": "...", "file_types": ["py"], "path": "."},
671
+ "status": "completed",
672
+ "results": [
673
+ {"file": "file1.py", "line_number": 10, "line": "..."},
674
+ ...
675
+ ],
676
+ "command": "rg ... (또는 grep ...)"
677
+ }
678
+ ```
679
+
680
+ **특징**:
681
+ - ripgrep 우선 사용 (속도)
682
+ - 없으면 grep 사용
683
+
684
+ #### `search_notebook_cells_tool`
685
+ Jupyter 노트북 셀에서 패턴을 검색합니다.
686
+
687
+ **파라미터**:
688
+ - `pattern`: 정규식 패턴
689
+ - `notebook_path`: 노트북 경로 (선택, 없으면 전체)
690
+
691
+ **반환**:
692
+ ```python
693
+ {
694
+ "tool": "search_notebook_cells",
695
+ "parameters": {"pattern": "...", "notebook_path": "..."},
696
+ "status": "completed",
697
+ "results": [
698
+ {
699
+ "notebook": "analysis.ipynb",
700
+ "cell_index": 3,
701
+ "cell_type": "code",
702
+ "source": "...",
703
+ "matches": [...]
704
+ },
705
+ ...
706
+ ]
707
+ }
708
+ ```
709
+
710
+ ---
711
+
712
+ ### Shell Tools (`shell_tools.py`)
713
+
714
+ #### `execute_command_tool`
715
+ 쉘 명령을 실행합니다.
716
+
717
+ **파라미터**:
718
+ - `command`: 쉘 명령
719
+ - `stdin`: 인터랙티브 프롬프트 입력 (기본 "y\n")
720
+ - `timeout`: 타임아웃 (밀리초, 기본 600000)
721
+ - `execution_result`: 클라이언트에서 실행한 결과 (HITL 후)
722
+
723
+ **반환**:
724
+ ```python
725
+ {
726
+ "tool": "execute_command_tool",
727
+ "parameters": {"command": "...", "stdin": "y\n", "timeout": 600000},
728
+ "status": "pending_execution", # 또는 "complete"
729
+ "message": "Shell command queued...",
730
+ "execution_result": {...} # HITL 후
731
+ }
732
+ ```
733
+
734
+ **특징**:
735
+ - HITL 대상 (사용자 승인 필요)
736
+ - 장시간 실행 명령 금지 (프롬프트에 명시)
737
+
738
+ ---
739
+
740
+ ### Resource Tools (`resource_tools.py`)
741
+
742
+ #### `check_resource_tool`
743
+ 파일 크기 및 DataFrame 메모리 사용량을 확인합니다.
744
+
745
+ **파라미터**:
746
+ - `files`: 파일 경로 리스트
747
+ - `dataframes`: DataFrame 변수명 리스트
748
+
749
+ **반환**:
750
+ ```python
751
+ {
752
+ "tool": "check_resource_tool",
753
+ "parameters": {"files": ["titanic.csv"], "dataframes": ["df"]},
754
+ "status": "pending_execution", # 클라이언트에서 실행
755
+ "message": "Resource check queued...",
756
+ "execution_result": {
757
+ "file_sizes": {"titanic.csv": "60KB"},
758
+ "dataframe_memory": {"df": "2.5MB"}
759
+ }
760
+ }
761
+ ```
762
+
763
+ **특징**:
764
+ - 클라이언트에서 실행 (Jupyter Extension의 CheckResourceHandler)
765
+ - 대용량 파일 로드 전 확인
766
+
767
+ ---
768
+
769
+ ## 실행 흐름
770
+
771
+ ### 1. 초기화 흐름
772
+
773
+ ```python
774
+ # 1. 라우터에서 에이전트 생성
775
+ agent = create_simple_chat_agent(
776
+ llm_config=llm_config,
777
+ workspace_root=workspace_root,
778
+ enable_hitl=True,
779
+ enable_todo_list=True,
780
+ checkpointer=checkpointer,
781
+ system_prompt_override=None
782
+ )
783
+
784
+ # 2. create_simple_chat_agent 내부
785
+ llm = create_llm(llm_config) # Gemini/OpenAI/vLLM
786
+ tools = _get_all_tools()
787
+
788
+ # 3. Middleware 체인 구성
789
+ middleware = [
790
+ handle_empty_response,
791
+ limit_tool_calls,
792
+ inject_continuation,
793
+ patch_tool_calls,
794
+ TodoListMiddleware(...),
795
+ HumanInTheLoopMiddleware(...),
796
+ ModelCallLimitMiddleware(run_limit=30),
797
+ ToolCallLimitMiddleware(...),
798
+ SummarizationMiddleware(...)
799
+ ]
800
+
801
+ # 4. 시스템 프롬프트 설정
802
+ system_prompt = DEFAULT_SYSTEM_PROMPT
803
+ if "gemini-2.5-flash" in llm_config.get("gemini", {}).get("model", ""):
804
+ system_prompt += GEMINI_CONTENT_PROMPT
805
+
806
+ # 5. 에이전트 생성
807
+ agent = create_agent(
808
+ model=llm,
809
+ tools=tools,
810
+ middleware=middleware,
811
+ checkpointer=checkpointer,
812
+ system_prompt=system_prompt
813
+ )
814
+ ```
815
+
816
+ ### 2. 스트리밍 흐름
817
+
818
+ ```python
819
+ # 1. 라우터에서 스트리밍 시작
820
+ async for step in agent.stream(agent_input, config):
821
+ # 2. step은 딕셔너리 {"messages": [...], "todos": [...]}
822
+
823
+ # 3. todos 처리
824
+ if "todos" in step:
825
+ todos = step["todos"]
826
+ yield {"event": "todos", "data": json.dumps({"todos": todos})}
827
+
828
+ # 4. messages 처리
829
+ if "messages" in step:
830
+ last_message = step["messages"][-1]
831
+
832
+ # 5. ToolMessage 처리
833
+ if isinstance(last_message, ToolMessage):
834
+ tool_name = last_message.name
835
+
836
+ if tool_name == "final_answer_tool":
837
+ # 6. final_answer 추출
838
+ tool_result = json.loads(last_message.content)
839
+ final_answer = tool_result.get("answer")
840
+ summary = tool_result.get("summary")
841
+
842
+ # 7. summary에서 next_items 추출
843
+ if summary:
844
+ summary_json = json.loads(summary)
845
+ if "next_items" in summary_json:
846
+ next_items_block = f"\n\n```json\n{json.dumps(summary_json)}\n```"
847
+ final_answer += next_items_block
848
+
849
+ # 8. 응답 전송
850
+ yield {"event": "token", "data": {"content": final_answer}}
851
+ yield {"event": "complete", "data": {"success": True}}
852
+ return
853
+
854
+ # 9. AIMessage 처리
855
+ elif isinstance(last_message, AIMessage):
856
+ # 10. tool_calls 확인
857
+ if last_message.tool_calls:
858
+ for tool_call in last_message.tool_calls:
859
+ # 11. 디버그 이벤트
860
+ yield {"event": "debug", "data": {"status": f"🔧 Tool 실행: {tool_call['name']}"}}
861
+
862
+ # 12. HITL 도구이면 tool_call 이벤트
863
+ if tool_call["name"] in HITL_TOOLS:
864
+ yield {"event": "tool_call", "data": tool_call}
865
+
866
+ # 13. content 전송
867
+ if last_message.content:
868
+ yield {"event": "token", "data": {"content": last_message.content}}
869
+ ```
870
+
871
+ ### 3. HITL 인터럽트 흐름
872
+
873
+ ```python
874
+ # 1. HumanInTheLoopMiddleware에서 인터럽트 발생
875
+ # LangGraph는 interrupt를 state에 저장하고 스트리밍 종료
876
+
877
+ # 2. 라우터에서 인터럽트 감지
878
+ if "__interrupt__" in step:
879
+ interrupt_data = step["__interrupt__"]
880
+ yield {"event": "interrupt", "data": interrupt_data}
881
+ return # 스트리밍 종료
882
+
883
+ # 3. 클라이언트에서 사용자 결정 대기
884
+ # - jupyter_cell_tool: UI에서 승인/거부
885
+ # - execute_command_tool/check_resource_tool: 서버에서 실행
886
+
887
+ # 4. 클라이언트가 resume_agent() 호출
888
+ POST /agent/langchain/resume
889
+ {
890
+ "threadId": "uuid",
891
+ "decision": "approved",
892
+ "execution_result": {...}
893
+ }
894
+
895
+ # 5. resume_agent에서 인터럽트 메시지 업데이트
896
+ interrupt_message.args["execution_result"] = execution_result
897
+
898
+ # 6. 에이전트 재개
899
+ agent.stream(None, config) # None은 새 입력 없음을 의미
900
+ ```
901
+
902
+ ### 4. 에이전트 종료 흐름
903
+
904
+ ```python
905
+ # 1. LLM이 final_answer_tool 호출
906
+ AIMessage(tool_calls=[{
907
+ "name": "final_answer_tool",
908
+ "args": {"answer": "...", "summary": "..."}
909
+ }])
910
+
911
+ # 2. final_answer_tool 실행
912
+ result = {
913
+ "tool": "final_answer",
914
+ "parameters": {...},
915
+ "status": "complete"
916
+ }
917
+
918
+ # 3. ToolMessage 생성
919
+ ToolMessage(name="final_answer_tool", content=json.dumps(result))
920
+
921
+ # 4. LangGraph가 ToolMessage를 LLM에 전달
922
+ # LLM이 빈 응답 반환 (도구 호출 없음)
923
+
924
+ # 5. handle_empty_response 미들웨어
925
+ # 마지막 메시지가 final_answer_tool이면 → 그대로 반환
926
+ # synthetic answer 생성하지 않음
927
+
928
+ # 6. LangGraph가 도구 호출 없는 응답 받고 종료
929
+ # agent.stream() 루프 종료
930
+
931
+ # 7. 라우터에서 complete 이벤트 전송
932
+ yield {"event": "complete", "data": {"success": True}}
933
+ return
934
+ ```
935
+
936
+ ---
937
+
938
+ ## 주요 설계 결정 사항
939
+
940
+ ### 1. Gemini 2.5 Flash 대응
941
+ - **문제**: content 빈값, multimodal 응답 (리스트)
942
+ - **해결**:
943
+ - 시스템 프롬프트에 content 포함 지시 추가
944
+ - `parse_json_tool_call`에서 리스트 처리
945
+
946
+ ### 2. final_answer_tool 반복 호출 방지
947
+ - **문제**: `final_answer_tool` 호출 후에도 에이전트 계속 실행
948
+ - **해결**:
949
+ - `ToolCallLimitMiddleware` 제거 (스레드 전체 카운트 문제)
950
+ - `handle_empty_response`에서 `final_answer_tool` 후 synthetic answer 생성 안함
951
+ - 에이전트가 자연스럽게 종료
952
+
953
+ ### 3. next_items UI 누락 문제
954
+ - **문제**: Gemini가 `summary` 필드에 JSON 문자열로 `next_items` 전달
955
+ - **해결**:
956
+ - 라우터에서 `summary` 필드 파싱
957
+ - `next_items` JSON을 마크다운 코드 블록으로 변환
958
+ - UI의 `extractNextItemsBlock` 함수가 파싱
959
+
960
+ ### 4. HITL 도구 vs non-HITL 도구
961
+ - **HITL**: 사용자 승인 필요
962
+ - `jupyter_cell_tool`, `execute_command_tool`, `write_file_tool`
963
+ - **non-HITL**: 즉시 실행
964
+ - `markdown_tool`, `read_file_tool`, `list_files_tool`, `search_*_tool`
965
+ - **클라이언트 실행**: 서버에서 실행하지 않음
966
+ - `check_resource_tool`: CheckResourceHandler에서 처리
967
+
968
+ ### 5. Checkpointer (InMemorySaver)
969
+ - 스레드별로 대화 상태 저장
970
+ - HITL 인터럽트 재개에 필수
971
+ - 메모리 기반 (서버 재시작 시 초기화)
972
+
973
+ ### 6. SSE 스트리밍 이벤트
974
+ - `todos`: TodoList 업데이트
975
+ - `token`: LLM 응답 토큰
976
+ - `debug`: 디버그 메시지 (도구 실행 상태)
977
+ - `tool_call`: HITL 도구 호출 요청
978
+ - `interrupt`: HITL 인터럽트 발생
979
+ - `complete`: 완료
980
+ - `debug_clear`: 디버그 메시지 클리어
981
+
982
+ ---
983
+
984
+ ## 디버깅 가이드
985
+
986
+ ### 로그 확인
987
+
988
+ #### Agent Server
989
+ ```bash
990
+ # 전체 로그
991
+ tail -f agent-server.log
992
+
993
+ # LLM 호출 로그
994
+ grep "AGENT -> LLM PROMPT" agent-server.log
995
+
996
+ # 미들웨어 로그
997
+ grep "Middleware:" agent-server.log
998
+
999
+ # 도구 실행 로그
1000
+ grep "Tool 실행:" agent-server.log
1001
+ ```
1002
+
1003
+ #### Jupyter Extension
1004
+ ```bash
1005
+ # Jupyter 서버 로그
1006
+ jupyter lab --debug
1007
+ ```
1008
+
1009
+ ### 주요 로그 패턴
1010
+
1011
+ #### 1. LLM 프롬프트
1012
+ ```
1013
+ ================================================================================================
1014
+ AGENT -> LLM PROMPT SYSTEM (1521 chars)
1015
+ ================================================================================================
1016
+ You are an expert Python data scientist...
1017
+
1018
+ ================================================================================================
1019
+ AGENT -> LLM PROMPT USER MESSAGES (batch=0)
1020
+ ================================================================================================
1021
+ [0] HumanMessage
1022
+ "타이타닉 데이터 분석해줘"
1023
+ ```
1024
+
1025
+ #### 2. LLM 응답
1026
+ ```
1027
+ ================================================================================================
1028
+ AGENT <- LLM RESPONSE
1029
+ ================================================================================================
1030
+ AIMessage
1031
+ {
1032
+ "content": "데이터를 로드하겠습니다.",
1033
+ "tool_calls": [
1034
+ {
1035
+ "name": "jupyter_cell_tool",
1036
+ "args": {"code": "import pandas as pd\ndf = pd.read_csv('titanic.csv')"}
1037
+ }
1038
+ ]
1039
+ }
1040
+ ```
1041
+
1042
+ #### 3. 미들웨어 실행
1043
+ ```
1044
+ Middleware: handle_empty_response [START]
1045
+ handle_empty_response: attempt=1, type=AIMessage, content=True, tool_calls=True
1046
+ Middleware: handle_empty_response [FINISH]
1047
+
1048
+ Middleware: inject_continuation_after_non_hitl_tool [START]
1049
+ Injecting continuation prompt after non-HITL tool: write_todos
1050
+ Middleware: inject_continuation_after_non_hitl_tool [FINISH]
1051
+ ```
1052
+
1053
+ #### 4. 도구 호출
1054
+ ```
1055
+ SSE: Emitting debug event for tool: jupyter_cell_tool
1056
+ 🔧 Tool 실행: jupyter_cell_tool
1057
+ ```
1058
+
1059
+ #### 5. HITL 인터럽트
1060
+ ```
1061
+ SimpleAgent interrupt detected with value: {...}
1062
+ SSE: Sending interrupt event
1063
+ ```
1064
+
1065
+ ### 트러블슈팅
1066
+
1067
+ #### 문제: 에이전트가 빈 응답만 반환
1068
+ - **원인**: Gemini 2.5 Flash의 빈 content
1069
+ - **확인**: `handle_empty_response` 로그에서 `content=False, tool_calls=False`
1070
+ - **해결**: 시스템 프롬프트에 content 포함 지시 추가됨
1071
+
1072
+ #### 문제: final_answer_tool이 반복 호출
1073
+ - **원인**: `handle_empty_response`가 synthetic answer 생성
1074
+ - **확인**: 로그에서 `"Synthesizing final_answer response."`
1075
+ - **해결**: `final_answer_tool` 후 synthetic answer 생성 안하도록 수정됨
1076
+
1077
+ #### 문제: next_items UI가 표시되지 않음
1078
+ - **원인**: Gemini가 `summary` 필드에 JSON 문자열로 전달
1079
+ - **확인**: ToolMessage content에서 `"summary": "{\"next_items\": [...]}"` 확인
1080
+ - **해결**: 라우터에서 `summary` 파싱 로직 추가됨
1081
+
1082
+ #### 문제: HITL 인터럽트 후 재개되지 않음
1083
+ - **원인**: Checkpointer에 상태 없음
1084
+ - **확인**: `resume_agent`에서 "No existing state for thread" 로그
1085
+ - **해결**: `stream_agent`에서 Checkpointer 생성 확인
1086
+
1087
+ ---
1088
+
1089
+ ## 확장 가이드
1090
+
1091
+ ### 새 도구 추가
1092
+
1093
+ 1. `tools/` 디렉토리에 파일 생성 (예: `custom_tools.py`)
1094
+ 2. `@tool` 데코레이터로 함수 정의
1095
+ 3. `tools/__init__.py`에서 export
1096
+ 4. `agent.py`의 `_get_all_tools()`에 추가
1097
+
1098
+ ```python
1099
+ # tools/custom_tools.py
1100
+ from langchain_core.tools import tool
1101
+ from pydantic import BaseModel, Field
1102
+
1103
+ class MyToolInput(BaseModel):
1104
+ param: str = Field(description="Parameter description")
1105
+
1106
+ @tool(args_schema=MyToolInput)
1107
+ def my_tool(param: str) -> Dict[str, Any]:
1108
+ """Tool description for LLM."""
1109
+ return {
1110
+ "tool": "my_tool",
1111
+ "parameters": {"param": param},
1112
+ "status": "completed",
1113
+ "result": "..."
1114
+ }
1115
+
1116
+ # tools/__init__.py
1117
+ from .custom_tools import my_tool
1118
+
1119
+ # agent.py
1120
+ def _get_all_tools():
1121
+ return [
1122
+ jupyter_cell_tool,
1123
+ markdown_tool,
1124
+ final_answer_tool,
1125
+ my_tool, # 추가
1126
+ ...
1127
+ ]
1128
+ ```
1129
+
1130
+ ### 새 미들웨어 추가
1131
+
1132
+ ```python
1133
+ # custom_middleware.py
1134
+ def create_my_middleware(wrap_model_call):
1135
+ @wrap_model_call
1136
+ @_with_middleware_logging("my_middleware")
1137
+ def my_middleware(request, handler):
1138
+ # 전처리
1139
+ logger.info("Before LLM call")
1140
+
1141
+ # LLM 호출
1142
+ response = handler(request)
1143
+
1144
+ # 후처리
1145
+ logger.info("After LLM call")
1146
+
1147
+ return response
1148
+
1149
+ return my_middleware
1150
+
1151
+ # agent.py
1152
+ def create_simple_chat_agent(...):
1153
+ ...
1154
+ my_middleware = create_my_middleware(wrap_model_call)
1155
+ middleware.append(my_middleware)
1156
+ ...
1157
+ ```
1158
+
1159
+ ### 새 LLM Provider 추가
1160
+
1161
+ ```python
1162
+ # llm_factory.py
1163
+ def _create_custom_llm(llm_config: Dict[str, Any], callbacks):
1164
+ from custom_llm_package import CustomLLM
1165
+
1166
+ custom_config = llm_config.get("custom", {})
1167
+ api_key = custom_config.get("apiKey")
1168
+ model = custom_config.get("model", "default-model")
1169
+
1170
+ return CustomLLM(
1171
+ model=model,
1172
+ api_key=api_key,
1173
+ temperature=0.0,
1174
+ callbacks=callbacks
1175
+ )
1176
+
1177
+ def create_llm(llm_config: Dict[str, Any]):
1178
+ provider = llm_config.get("provider", "gemini")
1179
+
1180
+ if provider == "custom":
1181
+ return _create_custom_llm(llm_config, callbacks)
1182
+ ...
1183
+ ```
1184
+
1185
+ ---
1186
+
1187
+ ## 참고 자료
1188
+
1189
+ - [LangChain Documentation](https://python.langchain.com/docs/get_started/introduction)
1190
+ - [LangGraph Documentation](https://langchain-ai.github.io/langgraph/)
1191
+ - [LangChain Agent Middleware](https://python.langchain.com/docs/modules/agents/middleware/)
1192
+ - [FastAPI SSE](https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse)
1193
+ - [Jupyter Server Extension](https://jupyter-server.readthedocs.io/en/latest/developers/extensions.html)