hdsp-jupyter-extension 2.0.5__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 -551
- 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 +941 -305
- hdsp_agent_core/prompts/auto_agent_prompts.py +3 -3
- {hdsp_jupyter_extension-2.0.5.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.5.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
- hdsp_jupyter_extension-2.0.5.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8cc4873c413ed56ff485.js → hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js +314 -8
- 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.5.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.5.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.37299706f55c6d46099d.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.5.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.5.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.5.dist-info → hdsp_jupyter_extension-2.0.7.dist-info}/METADATA +2 -1
- {hdsp_jupyter_extension-2.0.5.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.8cc4873c413ed56ff485.js → frontend_styles_index_js.4770ec0fb2d173b6deb4.js} +314 -8
- 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.37299706f55c6d46099d.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.5.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.5.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8cc4873c413ed56ff485.js.map +0 -1
- hdsp_jupyter_extension-2.0.5.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
- hdsp_jupyter_extension-2.0.5.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.37299706f55c6d46099d.js.map +0 -1
- hdsp_jupyter_extension-2.0.5.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.5.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.5.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.8cc4873c413ed56ff485.js.map +0 -1
- jupyter_ext/labextension/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
- jupyter_ext/labextension/static/remoteEntry.37299706f55c6d46099d.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.5.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.5.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.5.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.5.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.5.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.5.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.5.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.5.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.5.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.5.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.5.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.5.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.5.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.5.dist-info → hdsp_jupyter_extension-2.0.7.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.5.dist-info → hdsp_jupyter_extension-2.0.7.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)
|