hdsp-jupyter-extension 2.0.8__py3-none-any.whl → 2.0.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_server/core/notebook_generator.py +4 -4
- agent_server/core/rag_manager.py +12 -3
- agent_server/core/retriever.py +2 -1
- agent_server/core/vllm_embedding_service.py +8 -5
- agent_server/langchain/ARCHITECTURE.md +7 -51
- agent_server/langchain/agent.py +31 -20
- agent_server/langchain/custom_middleware.py +234 -31
- agent_server/langchain/hitl_config.py +5 -8
- agent_server/langchain/logging_utils.py +7 -7
- agent_server/langchain/prompts.py +106 -120
- agent_server/langchain/tools/__init__.py +1 -10
- agent_server/langchain/tools/file_tools.py +9 -61
- agent_server/langchain/tools/jupyter_tools.py +0 -1
- agent_server/langchain/tools/lsp_tools.py +8 -8
- agent_server/langchain/tools/resource_tools.py +12 -12
- agent_server/langchain/tools/search_tools.py +3 -158
- agent_server/prompts/file_action_prompts.py +8 -8
- agent_server/routers/langchain_agent.py +200 -125
- hdsp_agent_core/__init__.py +46 -47
- hdsp_agent_core/factory.py +6 -10
- hdsp_agent_core/interfaces.py +4 -2
- hdsp_agent_core/knowledge/__init__.py +5 -5
- hdsp_agent_core/knowledge/chunking.py +87 -61
- hdsp_agent_core/knowledge/loader.py +103 -101
- hdsp_agent_core/llm/service.py +192 -107
- hdsp_agent_core/managers/config_manager.py +16 -22
- hdsp_agent_core/managers/session_manager.py +5 -4
- hdsp_agent_core/models/__init__.py +12 -12
- hdsp_agent_core/models/agent.py +15 -8
- hdsp_agent_core/models/common.py +1 -2
- hdsp_agent_core/models/rag.py +48 -111
- hdsp_agent_core/prompts/__init__.py +12 -12
- hdsp_agent_core/prompts/cell_action_prompts.py +9 -7
- hdsp_agent_core/services/agent_service.py +10 -8
- hdsp_agent_core/services/chat_service.py +10 -6
- hdsp_agent_core/services/rag_service.py +3 -6
- hdsp_agent_core/tests/conftest.py +4 -1
- hdsp_agent_core/tests/test_factory.py +2 -2
- hdsp_agent_core/tests/test_services.py +12 -19
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
- hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js → hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js +93 -4
- hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +1 -0
- hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js → hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.58c1e128ba0b76f41f04.js +153 -130
- hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.58c1e128ba0b76f41f04.js.map +1 -0
- hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js → hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.9da31d1134a53b0c4af5.js +6 -6
- hdsp_jupyter_extension-2.0.11.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.9da31d1134a53b0c4af5.js.map +1 -0
- {hdsp_jupyter_extension-2.0.8.dist-info → hdsp_jupyter_extension-2.0.11.dist-info}/METADATA +1 -3
- hdsp_jupyter_extension-2.0.11.dist-info/RECORD +144 -0
- jupyter_ext/__init__.py +21 -11
- jupyter_ext/_version.py +1 -1
- jupyter_ext/handlers.py +69 -50
- jupyter_ext/labextension/build_log.json +1 -1
- jupyter_ext/labextension/package.json +2 -2
- jupyter_ext/labextension/static/{frontend_styles_index_js.8740a527757068814573.js → frontend_styles_index_js.2d9fb488c82498c45c2d.js} +93 -4
- jupyter_ext/labextension/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +1 -0
- jupyter_ext/labextension/static/{lib_index_js.e4ff4b5779b5e049f84c.js → lib_index_js.58c1e128ba0b76f41f04.js} +153 -130
- jupyter_ext/labextension/static/lib_index_js.58c1e128ba0b76f41f04.js.map +1 -0
- jupyter_ext/labextension/static/{remoteEntry.020cdb0b864cfaa4e41e.js → remoteEntry.9da31d1134a53b0c4af5.js} +6 -6
- jupyter_ext/labextension/static/remoteEntry.9da31d1134a53b0c4af5.js.map +1 -0
- hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js.map +0 -1
- hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +0 -1
- hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +0 -1
- hdsp_jupyter_extension-2.0.8.dist-info/RECORD +0 -144
- jupyter_ext/labextension/static/frontend_styles_index_js.8740a527757068814573.js.map +0 -1
- jupyter_ext/labextension/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +0 -1
- jupyter_ext/labextension/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +0 -1
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js.map +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js.map +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js.map +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js.map +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +0 -0
- {hdsp_jupyter_extension-2.0.8.data → hdsp_jupyter_extension-2.0.11.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -0
- {hdsp_jupyter_extension-2.0.8.dist-info → hdsp_jupyter_extension-2.0.11.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.8.dist-info → hdsp_jupyter_extension-2.0.11.dist-info}/licenses/LICENSE +0 -0
|
@@ -118,9 +118,9 @@ JSON만 응답하세요:"""
|
|
|
118
118
|
요청: {prompt}
|
|
119
119
|
|
|
120
120
|
분석 결과:
|
|
121
|
-
- 목표: {analysis[
|
|
122
|
-
- 라이브러리: {
|
|
123
|
-
- 분석 유형: {analysis[
|
|
121
|
+
- 목표: {analysis["objective"]}
|
|
122
|
+
- 라이브러리: {", ".join(analysis["libraries"])}
|
|
123
|
+
- 분석 유형: {analysis["analysis_type"]}
|
|
124
124
|
|
|
125
125
|
노트북의 각 셀에 대한 계획을 JSON 형식으로 작성해주세요:
|
|
126
126
|
{{
|
|
@@ -171,7 +171,7 @@ JSON만 응답하세요:"""
|
|
|
171
171
|
# Update progress (30% -> 80%)
|
|
172
172
|
progress = 30 + int((idx / total_cells) * 50)
|
|
173
173
|
self.task_manager.update_progress(
|
|
174
|
-
task_id, progress, f"셀 생성 중... ({idx+1}/{total_cells})"
|
|
174
|
+
task_id, progress, f"셀 생성 중... ({idx + 1}/{total_cells})"
|
|
175
175
|
)
|
|
176
176
|
|
|
177
177
|
cell_type = cell_plan.get("type", "code")
|
agent_server/core/rag_manager.py
CHANGED
|
@@ -90,16 +90,25 @@ class RAGManager:
|
|
|
90
90
|
|
|
91
91
|
# 2. Initialize embedding service (local or vLLM backend)
|
|
92
92
|
import os
|
|
93
|
-
|
|
93
|
+
|
|
94
|
+
embedding_backend = os.environ.get(
|
|
95
|
+
"HDSP_EMBEDDING_BACKEND", "local"
|
|
96
|
+
).lower()
|
|
94
97
|
|
|
95
98
|
if embedding_backend == "vllm":
|
|
96
|
-
from agent_server.core.vllm_embedding_service import
|
|
97
|
-
|
|
99
|
+
from agent_server.core.vllm_embedding_service import (
|
|
100
|
+
get_vllm_embedding_service,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
self._embedding_service = get_vllm_embedding_service(
|
|
104
|
+
self._config.embedding
|
|
105
|
+
)
|
|
98
106
|
logger.info(
|
|
99
107
|
f"vLLM Embedding service initialized (dim={self._embedding_service.dimension})"
|
|
100
108
|
)
|
|
101
109
|
else:
|
|
102
110
|
from agent_server.core.embedding_service import get_embedding_service
|
|
111
|
+
|
|
103
112
|
self._embedding_service = get_embedding_service(self._config.embedding)
|
|
104
113
|
# Load model to get dimension
|
|
105
114
|
await self._embedding_service._ensure_model_loaded()
|
agent_server/core/retriever.py
CHANGED
|
@@ -96,7 +96,8 @@ class Retriever:
|
|
|
96
96
|
query=query_embedding,
|
|
97
97
|
query_filter=qdrant_filter,
|
|
98
98
|
limit=effective_top_k,
|
|
99
|
-
score_threshold=effective_threshold
|
|
99
|
+
score_threshold=effective_threshold
|
|
100
|
+
* 0.5, # Lower for initial retrieval
|
|
100
101
|
with_payload=True,
|
|
101
102
|
with_vectors=False,
|
|
102
103
|
)
|
|
@@ -17,7 +17,6 @@ import os
|
|
|
17
17
|
from typing import TYPE_CHECKING, List, Optional
|
|
18
18
|
|
|
19
19
|
import httpx
|
|
20
|
-
import time
|
|
21
20
|
|
|
22
21
|
if TYPE_CHECKING:
|
|
23
22
|
from hdsp_agent_core.models.rag import EmbeddingConfig
|
|
@@ -66,7 +65,7 @@ class VLLMEmbeddingService:
|
|
|
66
65
|
self._client = httpx.AsyncClient(
|
|
67
66
|
base_url=self._endpoint,
|
|
68
67
|
timeout=httpx.Timeout(30.0),
|
|
69
|
-
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10)
|
|
68
|
+
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10),
|
|
70
69
|
)
|
|
71
70
|
|
|
72
71
|
logger.info(
|
|
@@ -79,7 +78,9 @@ class VLLMEmbeddingService:
|
|
|
79
78
|
"""Get embedding dimension"""
|
|
80
79
|
return self._dimension
|
|
81
80
|
|
|
82
|
-
async def _call_vllm_api(
|
|
81
|
+
async def _call_vllm_api(
|
|
82
|
+
self, texts: List[str], max_retries: int = 3
|
|
83
|
+
) -> List[List[float]]:
|
|
83
84
|
"""
|
|
84
85
|
Call vLLM embedding API with retry logic.
|
|
85
86
|
|
|
@@ -126,7 +127,9 @@ class VLLMEmbeddingService:
|
|
|
126
127
|
logger.error(f"Unexpected error calling vLLM API: {e}")
|
|
127
128
|
break
|
|
128
129
|
|
|
129
|
-
raise Exception(
|
|
130
|
+
raise Exception(
|
|
131
|
+
f"Failed to connect to vLLM after {max_retries} attempts: {last_error}"
|
|
132
|
+
)
|
|
130
133
|
|
|
131
134
|
async def embed_texts(self, texts: List[str]) -> List[List[float]]:
|
|
132
135
|
"""
|
|
@@ -240,4 +243,4 @@ def reset_vllm_embedding_service() -> None:
|
|
|
240
243
|
_vllm_embedding_service._initialized = False
|
|
241
244
|
_vllm_embedding_service = None
|
|
242
245
|
VLLMEmbeddingService._instance = None
|
|
243
|
-
VLLMEmbeddingService._initialized = False
|
|
246
|
+
VLLMEmbeddingService._initialized = False
|
|
@@ -151,13 +151,10 @@ jupyter_ext/
|
|
|
151
151
|
```python
|
|
152
152
|
- jupyter_cell_tool # Python 코드 실행
|
|
153
153
|
- markdown_tool # 마크다운 셀 추가
|
|
154
|
-
- final_answer_tool # 작업 완료 및 요약
|
|
155
154
|
- read_file_tool # 파일 읽기
|
|
156
155
|
- write_file_tool # 파일 쓰기
|
|
157
|
-
- list_files_tool # 디렉토리 목록
|
|
158
|
-
- search_workspace_tool # 워크스페이스 검색 (grep/rg)
|
|
159
156
|
- search_notebook_cells_tool # 노트북 셀 검색
|
|
160
|
-
- execute_command_tool # 쉘 명령 실행
|
|
157
|
+
- execute_command_tool # 쉘 명령 실행 (파일 검색은 find/grep 사용)
|
|
161
158
|
- check_resource_tool # 리소스 확인
|
|
162
159
|
```
|
|
163
160
|
|
|
@@ -441,8 +438,6 @@ non-HITL 도구 실행 후 continuation 프롬프트를 주입합니다.
|
|
|
441
438
|
NON_HITL_TOOLS = {
|
|
442
439
|
"markdown_tool",
|
|
443
440
|
"read_file_tool",
|
|
444
|
-
"list_files_tool",
|
|
445
|
-
"search_workspace_tool",
|
|
446
441
|
"search_notebook_cells_tool",
|
|
447
442
|
"write_todos",
|
|
448
443
|
}
|
|
@@ -508,8 +503,7 @@ LLM 호출 횟수를 제한합니다.
|
|
|
508
503
|
|
|
509
504
|
**설정**:
|
|
510
505
|
```python
|
|
511
|
-
- write_todos: run_limit=
|
|
512
|
-
- list_files_tool: run_limit=5, exit_behavior="continue"
|
|
506
|
+
- write_todos: run_limit=20, exit_behavior="continue"
|
|
513
507
|
```
|
|
514
508
|
|
|
515
509
|
### 9. `SummarizationMiddleware` (LangChain 내장)
|
|
@@ -634,52 +628,14 @@ Python 코드를 Jupyter 셀에서 실행합니다.
|
|
|
634
628
|
**특징**:
|
|
635
629
|
- HITL 대상 (사용자 승인 필요)
|
|
636
630
|
|
|
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
631
|
---
|
|
655
632
|
|
|
656
633
|
### Search Tools (`search_tools.py`)
|
|
657
634
|
|
|
658
|
-
|
|
659
|
-
|
|
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 사용
|
|
635
|
+
> **Note**: 파일 검색 기능은 `execute_command_tool`을 통해 `find`/`grep` 명령을 직접 사용합니다.
|
|
636
|
+
>
|
|
637
|
+
> - 파일명 검색: `execute_command_tool(command="find . -iname '*pattern*' 2>/dev/null")`
|
|
638
|
+
> - 파일 내용 검색: `execute_command_tool(command="grep -rn 'pattern' --include='*.py' .")`
|
|
683
639
|
|
|
684
640
|
#### `search_notebook_cells_tool`
|
|
685
641
|
Jupyter 노트북 셀에서 패턴을 검색합니다.
|
|
@@ -961,7 +917,7 @@ return
|
|
|
961
917
|
- **HITL**: 사용자 승인 필요
|
|
962
918
|
- `jupyter_cell_tool`, `execute_command_tool`, `write_file_tool`
|
|
963
919
|
- **non-HITL**: 즉시 실행
|
|
964
|
-
- `markdown_tool`, `read_file_tool`, `
|
|
920
|
+
- `markdown_tool`, `read_file_tool`, `search_*_tool`
|
|
965
921
|
- **클라이언트 실행**: 서버에서 실행하지 않음
|
|
966
922
|
- `check_resource_tool`: CheckResourceHandler에서 처리
|
|
967
923
|
|
agent_server/langchain/agent.py
CHANGED
|
@@ -26,15 +26,12 @@ from agent_server.langchain.tools import (
|
|
|
26
26
|
diagnostics_tool,
|
|
27
27
|
edit_file_tool,
|
|
28
28
|
execute_command_tool,
|
|
29
|
-
final_answer_tool,
|
|
30
29
|
jupyter_cell_tool,
|
|
31
|
-
list_files_tool,
|
|
32
30
|
markdown_tool,
|
|
33
31
|
multiedit_file_tool,
|
|
34
32
|
read_file_tool,
|
|
35
33
|
references_tool,
|
|
36
34
|
search_notebook_cells_tool,
|
|
37
|
-
search_workspace_tool,
|
|
38
35
|
write_file_tool,
|
|
39
36
|
)
|
|
40
37
|
|
|
@@ -46,13 +43,10 @@ def _get_all_tools():
|
|
|
46
43
|
return [
|
|
47
44
|
jupyter_cell_tool,
|
|
48
45
|
markdown_tool,
|
|
49
|
-
final_answer_tool,
|
|
50
46
|
read_file_tool,
|
|
51
47
|
write_file_tool,
|
|
52
48
|
edit_file_tool,
|
|
53
49
|
multiedit_file_tool,
|
|
54
|
-
list_files_tool,
|
|
55
|
-
search_workspace_tool,
|
|
56
50
|
search_notebook_cells_tool,
|
|
57
51
|
execute_command_tool,
|
|
58
52
|
check_resource_tool,
|
|
@@ -115,7 +109,6 @@ def create_simple_chat_agent(
|
|
|
115
109
|
|
|
116
110
|
# Configure middleware
|
|
117
111
|
middleware = []
|
|
118
|
-
|
|
119
112
|
# Add empty response handler middleware
|
|
120
113
|
handle_empty_response = create_handle_empty_response_middleware(wrap_model_call)
|
|
121
114
|
middleware.append(handle_empty_response)
|
|
@@ -125,7 +118,9 @@ def create_simple_chat_agent(
|
|
|
125
118
|
middleware.append(limit_tool_calls)
|
|
126
119
|
|
|
127
120
|
# Add tool args normalization middleware (convert list args to strings based on schema)
|
|
128
|
-
normalize_tool_args = create_normalize_tool_args_middleware(
|
|
121
|
+
normalize_tool_args = create_normalize_tool_args_middleware(
|
|
122
|
+
wrap_model_call, tools=tools
|
|
123
|
+
)
|
|
129
124
|
middleware.append(normalize_tool_args)
|
|
130
125
|
|
|
131
126
|
# Add continuation prompt middleware
|
|
@@ -164,22 +159,14 @@ def create_simple_chat_agent(
|
|
|
164
159
|
logger.info("Added ModelCallLimitMiddleware with run_limit=30")
|
|
165
160
|
|
|
166
161
|
# ToolCallLimitMiddleware: Prevent specific tools from being called too many times
|
|
167
|
-
#
|
|
162
|
+
# run_limit resets automatically per user message
|
|
168
163
|
write_todos_limit = ToolCallLimitMiddleware(
|
|
169
164
|
tool_name="write_todos",
|
|
170
|
-
run_limit=
|
|
171
|
-
exit_behavior="continue", # Let agent continue with other tools
|
|
172
|
-
)
|
|
173
|
-
middleware.append(write_todos_limit)
|
|
174
|
-
|
|
175
|
-
# Limit list_files_tool to prevent excessive directory listing
|
|
176
|
-
list_files_limit = ToolCallLimitMiddleware(
|
|
177
|
-
tool_name="list_files_tool",
|
|
178
|
-
run_limit=5, # Max 5 list_files calls per user message
|
|
165
|
+
run_limit=20, # Max 20 write_todos calls per user message
|
|
179
166
|
exit_behavior="continue",
|
|
180
167
|
)
|
|
181
|
-
middleware.append(
|
|
182
|
-
logger.info("Added ToolCallLimitMiddleware for write_todos
|
|
168
|
+
middleware.append(write_todos_limit)
|
|
169
|
+
logger.info("Added ToolCallLimitMiddleware for write_todos (20/msg)")
|
|
183
170
|
|
|
184
171
|
# Add SummarizationMiddleware to maintain context across cycles
|
|
185
172
|
summary_llm = create_summarization_llm(llm_config)
|
|
@@ -218,6 +205,30 @@ Example: "데이터를 로드하겠습니다." then call jupyter_cell_tool.
|
|
|
218
205
|
system_prompt = system_prompt + "\n" + gemini_content_prompt
|
|
219
206
|
logger.info("Added Gemini 2.5 Flash specific prompt for content inclusion")
|
|
220
207
|
|
|
208
|
+
# Add vLLM/gpt-oss specific prompt for Korean responses and proper todo structure
|
|
209
|
+
provider = llm_config.get("provider", "")
|
|
210
|
+
if provider == "vllm":
|
|
211
|
+
vllm_prompt = """
|
|
212
|
+
## 🔴 중요: 한국어로 응답하세요
|
|
213
|
+
- 모든 응답, 설명, todo 항목은 반드시 한국어로 작성하세요.
|
|
214
|
+
- 코드 주석과 출력 설명도 한국어로 작성하세요.
|
|
215
|
+
- 영어로 응답하지 마세요.
|
|
216
|
+
|
|
217
|
+
## 🔴 MANDATORY: Todo List Structure
|
|
218
|
+
When creating todos with write_todos, you MUST:
|
|
219
|
+
1. Write all todo items in Korean
|
|
220
|
+
2. ALWAYS include "작업 요약 및 다음단계 제시" as the LAST todo item
|
|
221
|
+
3. Example structure:
|
|
222
|
+
- 데이터 로드 및 확인
|
|
223
|
+
- 데이터 분석 수행
|
|
224
|
+
- 작업 요약 및 다음단계 제시 ← 반드시 마지막에 포함!
|
|
225
|
+
|
|
226
|
+
## 🔴 IMPORTANT: Never return empty responses
|
|
227
|
+
If you have nothing to say, call a tool instead. NEVER return an empty response.
|
|
228
|
+
"""
|
|
229
|
+
system_prompt = system_prompt + "\n" + vllm_prompt
|
|
230
|
+
logger.info("Added vLLM/gpt-oss specific prompt for Korean responses")
|
|
231
|
+
|
|
221
232
|
logger.info("SimpleChatAgent system_prompt: %s", system_prompt)
|
|
222
233
|
|
|
223
234
|
# Create agent with checkpointer (required for HITL)
|
|
@@ -139,6 +139,45 @@ def create_handle_empty_response_middleware(wrap_model_call):
|
|
|
139
139
|
def handle_empty_response(request, handler):
|
|
140
140
|
max_retries = 2
|
|
141
141
|
|
|
142
|
+
# Check if all todos are completed - if so, return empty response to stop agent
|
|
143
|
+
# Method 1: Check state.todos
|
|
144
|
+
todos = request.state.get("todos", [])
|
|
145
|
+
if todos:
|
|
146
|
+
pending_todos = [
|
|
147
|
+
t for t in todos if t.get("status") in ("pending", "in_progress")
|
|
148
|
+
]
|
|
149
|
+
if not pending_todos:
|
|
150
|
+
logger.info(
|
|
151
|
+
"All %d todos completed (from state) - stopping agent (no LLM call)",
|
|
152
|
+
len(todos),
|
|
153
|
+
)
|
|
154
|
+
return AIMessage(content="", tool_calls=[])
|
|
155
|
+
|
|
156
|
+
# Method 2: Check last message if it's a write_todos ToolMessage with all completed
|
|
157
|
+
messages = request.messages
|
|
158
|
+
if messages:
|
|
159
|
+
last_msg = messages[-1]
|
|
160
|
+
if getattr(last_msg, "type", "") == "tool":
|
|
161
|
+
tool_name = getattr(last_msg, "name", "") or ""
|
|
162
|
+
content = getattr(last_msg, "content", "") or ""
|
|
163
|
+
if tool_name == "write_todos" or "Updated todo list to" in content:
|
|
164
|
+
# Extract todos from ToolMessage content
|
|
165
|
+
try:
|
|
166
|
+
import ast
|
|
167
|
+
if "Updated todo list to" in content:
|
|
168
|
+
list_text = content.split("Updated todo list to", 1)[1].strip()
|
|
169
|
+
todos_from_msg = ast.literal_eval(list_text)
|
|
170
|
+
if isinstance(todos_from_msg, list) and len(todos_from_msg) > 0:
|
|
171
|
+
pending = [t for t in todos_from_msg if t.get("status") in ("pending", "in_progress")]
|
|
172
|
+
if not pending:
|
|
173
|
+
logger.info(
|
|
174
|
+
"All %d todos completed (from ToolMessage) - stopping agent (no LLM call)",
|
|
175
|
+
len(todos_from_msg),
|
|
176
|
+
)
|
|
177
|
+
return AIMessage(content="", tool_calls=[])
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.debug("Failed to parse todos from ToolMessage: %s", e)
|
|
180
|
+
|
|
142
181
|
# Check if last message is final_answer_tool result - if so, don't retry/synthesize
|
|
143
182
|
# This allows agent to naturally terminate after final_answer_tool
|
|
144
183
|
messages = request.messages
|
|
@@ -206,6 +245,25 @@ def create_handle_empty_response_middleware(wrap_model_call):
|
|
|
206
245
|
# Invalid response - retry with JSON schema prompt
|
|
207
246
|
if response_message and attempt < max_retries:
|
|
208
247
|
reason = "text-only" if has_content else "empty"
|
|
248
|
+
|
|
249
|
+
json_prompt = _build_json_prompt(request, response_message, has_content)
|
|
250
|
+
|
|
251
|
+
# If _build_json_prompt returns None, skip retry and synthesize write_todos
|
|
252
|
+
# This happens when: all todos completed OR current todo is summary/next_steps
|
|
253
|
+
if json_prompt is None:
|
|
254
|
+
logger.info(
|
|
255
|
+
"Skipping retry for %s response, synthesizing write_todos with content",
|
|
256
|
+
reason,
|
|
257
|
+
)
|
|
258
|
+
# Synthesize write_todos while preserving the content (summary)
|
|
259
|
+
synthetic_message = _create_synthetic_final_answer(
|
|
260
|
+
request, response_message, has_content
|
|
261
|
+
)
|
|
262
|
+
response = _replace_ai_message_in_response(
|
|
263
|
+
response, synthetic_message
|
|
264
|
+
)
|
|
265
|
+
return response
|
|
266
|
+
|
|
209
267
|
logger.warning(
|
|
210
268
|
"Invalid AIMessage (%s) detected (attempt %d/%d). "
|
|
211
269
|
"Retrying with JSON schema prompt...",
|
|
@@ -214,16 +272,26 @@ def create_handle_empty_response_middleware(wrap_model_call):
|
|
|
214
272
|
max_retries + 1,
|
|
215
273
|
)
|
|
216
274
|
|
|
217
|
-
json_prompt = _build_json_prompt(request, response_message, has_content)
|
|
218
275
|
request = request.override(
|
|
219
276
|
messages=request.messages + [HumanMessage(content=json_prompt)]
|
|
220
277
|
)
|
|
221
278
|
continue
|
|
222
279
|
|
|
223
|
-
# Max retries exhausted - synthesize
|
|
280
|
+
# Max retries exhausted - synthesize write_todos to complete
|
|
224
281
|
if response_message:
|
|
282
|
+
# Check if todos are already all completed - if so, just return
|
|
283
|
+
todos = request.state.get("todos", [])
|
|
284
|
+
pending_todos = [
|
|
285
|
+
t for t in todos if t.get("status") in ("pending", "in_progress")
|
|
286
|
+
]
|
|
287
|
+
if todos and not pending_todos:
|
|
288
|
+
logger.info(
|
|
289
|
+
"Max retries exhausted but all todos completed - returning response as-is"
|
|
290
|
+
)
|
|
291
|
+
return response
|
|
292
|
+
|
|
225
293
|
logger.warning(
|
|
226
|
-
"Max retries exhausted. Synthesizing
|
|
294
|
+
"Max retries exhausted. Synthesizing write_todos to complete."
|
|
227
295
|
)
|
|
228
296
|
synthetic_message = _create_synthetic_final_answer(
|
|
229
297
|
request, response_message, has_content
|
|
@@ -274,14 +342,33 @@ def _build_json_prompt(request, response_message, has_content):
|
|
|
274
342
|
"""Build JSON-forcing prompt based on context."""
|
|
275
343
|
todos = request.state.get("todos", [])
|
|
276
344
|
pending_todos = [t for t in todos if t.get("status") in ("pending", "in_progress")]
|
|
345
|
+
in_progress_todos = [t for t in todos if t.get("status") == "in_progress"]
|
|
277
346
|
|
|
278
347
|
if has_content:
|
|
279
|
-
|
|
348
|
+
# If all todos completed, don't force another tool call
|
|
349
|
+
if todos and not pending_todos:
|
|
350
|
+
return None # Signal to skip retry
|
|
351
|
+
|
|
352
|
+
# If current in_progress todo is "작업 요약 및 다음단계 제시", accept text-only response
|
|
353
|
+
# The LLM is outputting the summary, we'll synthesize write_todos
|
|
354
|
+
if in_progress_todos:
|
|
355
|
+
current_todo = in_progress_todos[0].get("content", "")
|
|
356
|
+
if (
|
|
357
|
+
"작업 요약" in current_todo
|
|
358
|
+
or "다음단계" in current_todo
|
|
359
|
+
or "다음 단계" in current_todo
|
|
360
|
+
):
|
|
361
|
+
logger.info(
|
|
362
|
+
"Current todo is summary/next steps ('%s'), accepting text-only response",
|
|
363
|
+
current_todo[:30],
|
|
364
|
+
)
|
|
365
|
+
return None # Signal to skip retry - will synthesize write_todos with content
|
|
366
|
+
|
|
280
367
|
return (
|
|
281
368
|
f"{JSON_TOOL_SCHEMA}\n\n"
|
|
282
369
|
f"Your previous response was text, not JSON. "
|
|
283
|
-
f"
|
|
284
|
-
f'{{"tool": "
|
|
370
|
+
f"Call the next appropriate tool to continue.\n"
|
|
371
|
+
f'Example: {{"tool": "jupyter_cell_tool", "arguments": {{"code": "print(\'hello\')"}}}}'
|
|
285
372
|
)
|
|
286
373
|
elif pending_todos:
|
|
287
374
|
todo_list = ", ".join(t.get("content", "")[:20] for t in pending_todos[:3])
|
|
@@ -292,39 +379,62 @@ def _build_json_prompt(request, response_message, has_content):
|
|
|
292
379
|
f"Call jupyter_cell_tool with Python code to complete the next task.\n"
|
|
293
380
|
f"Example: {example_json}"
|
|
294
381
|
)
|
|
382
|
+
elif not todos:
|
|
383
|
+
# No todos yet = new task starting, LLM must create todos or call a tool
|
|
384
|
+
# This happens when LLM returns empty response at the start of a new task
|
|
385
|
+
logger.info("No todos exist yet - forcing retry to create todos or call tool")
|
|
386
|
+
return (
|
|
387
|
+
f"{JSON_TOOL_SCHEMA}\n\n"
|
|
388
|
+
f"Your response was empty. You MUST call a tool to proceed.\n"
|
|
389
|
+
f"한국어로 응답하고, write_todos로 작업 목록을 만들거나 jupyter_cell_tool/read_file_tool을 호출하세요.\n"
|
|
390
|
+
f'Example: {{"tool": "write_todos", "arguments": {{"todos": [{{"content": "데이터 분석", "status": "in_progress"}}]}}}}'
|
|
391
|
+
)
|
|
295
392
|
else:
|
|
393
|
+
# Todos exist but all completed - ask for summary
|
|
394
|
+
logger.info("All todos completed but response empty - asking for summary")
|
|
296
395
|
return (
|
|
297
396
|
f"{JSON_TOOL_SCHEMA}\n\n"
|
|
298
|
-
f"All tasks completed. Call
|
|
299
|
-
f
|
|
397
|
+
f"All tasks completed. Call markdown_tool to provide a summary in Korean.\n"
|
|
398
|
+
f"한국어로 작업 요약을 작성하세요.\n"
|
|
399
|
+
f'Example: {{"tool": "markdown_tool", "arguments": {{"content": "작업이 완료되었습니다."}}}}'
|
|
300
400
|
)
|
|
301
401
|
|
|
302
402
|
|
|
303
403
|
def _create_synthetic_final_answer(request, response_message, has_content):
|
|
304
|
-
"""Create synthetic
|
|
305
|
-
|
|
306
|
-
|
|
404
|
+
"""Create synthetic write_todos call to mark all todos as completed.
|
|
405
|
+
|
|
406
|
+
This triggers automatic session termination via router's all_todos_completed check.
|
|
407
|
+
Preserves the LLM's text content (summary) if present.
|
|
408
|
+
"""
|
|
409
|
+
todos = request.state.get("todos", [])
|
|
410
|
+
|
|
411
|
+
# Mark all todos as completed
|
|
412
|
+
completed_todos = (
|
|
413
|
+
[{**todo, "status": "completed"} for todo in todos]
|
|
414
|
+
if todos
|
|
415
|
+
else [{"content": "작업 완료", "status": "completed"}]
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Preserve original content (summary JSON) if present
|
|
419
|
+
original_content = ""
|
|
420
|
+
if has_content and response_message and response_message.content:
|
|
421
|
+
original_content = response_message.content
|
|
307
422
|
logger.info(
|
|
308
|
-
"
|
|
309
|
-
len(
|
|
423
|
+
"Creating synthetic write_todos with preserved content (length=%d)",
|
|
424
|
+
len(original_content),
|
|
310
425
|
)
|
|
311
426
|
else:
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
]
|
|
316
|
-
summary = (
|
|
317
|
-
f"작업이 완료되었습니다. 완료된 항목: {', '.join(completed_todos[:5])}"
|
|
318
|
-
if completed_todos
|
|
319
|
-
else "작업이 완료되었습니다."
|
|
427
|
+
logger.info(
|
|
428
|
+
"Creating synthetic write_todos to mark %d todos as completed",
|
|
429
|
+
len(completed_todos),
|
|
320
430
|
)
|
|
321
431
|
|
|
322
432
|
return AIMessage(
|
|
323
|
-
content=
|
|
433
|
+
content=original_content, # Preserve the summary content for UI
|
|
324
434
|
tool_calls=[
|
|
325
435
|
{
|
|
326
|
-
"name": "
|
|
327
|
-
"args": {"
|
|
436
|
+
"name": "write_todos",
|
|
437
|
+
"args": {"todos": completed_todos},
|
|
328
438
|
"id": str(uuid.uuid4()),
|
|
329
439
|
"type": "tool_call",
|
|
330
440
|
}
|
|
@@ -504,6 +614,69 @@ def create_normalize_tool_args_middleware(wrap_model_call, tools=None):
|
|
|
504
614
|
)
|
|
505
615
|
args[key] = normalized_value
|
|
506
616
|
|
|
617
|
+
# Ensure write_todos includes summary todo as last item
|
|
618
|
+
if tool_name == "write_todos" and "todos" in args:
|
|
619
|
+
todos = args["todos"]
|
|
620
|
+
if isinstance(todos, list) and len(todos) > 0:
|
|
621
|
+
# Check if any todo contains summary keywords
|
|
622
|
+
summary_keywords = [
|
|
623
|
+
"작업 요약",
|
|
624
|
+
"다음단계",
|
|
625
|
+
"다음 단계",
|
|
626
|
+
"요약 및",
|
|
627
|
+
]
|
|
628
|
+
has_summary = any(
|
|
629
|
+
any(
|
|
630
|
+
kw in todo.get("content", "")
|
|
631
|
+
for kw in summary_keywords
|
|
632
|
+
)
|
|
633
|
+
for todo in todos
|
|
634
|
+
if isinstance(todo, dict)
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
if not has_summary:
|
|
638
|
+
# Add summary todo as last item
|
|
639
|
+
summary_todo = {
|
|
640
|
+
"content": "작업 요약 및 다음단계 제시",
|
|
641
|
+
"status": "pending",
|
|
642
|
+
}
|
|
643
|
+
todos.append(summary_todo)
|
|
644
|
+
logger.info(
|
|
645
|
+
"Auto-added '작업 요약 및 다음단계 제시' to write_todos (total: %d todos)",
|
|
646
|
+
len(todos),
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
# Log warning if summary todo is completed without JSON (but don't block)
|
|
650
|
+
for todo in todos:
|
|
651
|
+
if not isinstance(todo, dict):
|
|
652
|
+
continue
|
|
653
|
+
content = todo.get("content", "")
|
|
654
|
+
status = todo.get("status", "")
|
|
655
|
+
is_summary_todo = any(
|
|
656
|
+
kw in content for kw in summary_keywords
|
|
657
|
+
)
|
|
658
|
+
if is_summary_todo and status == "completed":
|
|
659
|
+
# Check if AIMessage content has summary JSON
|
|
660
|
+
msg_content = getattr(msg, "content", "") or ""
|
|
661
|
+
if isinstance(msg_content, list):
|
|
662
|
+
msg_content = " ".join(
|
|
663
|
+
str(p) for p in msg_content
|
|
664
|
+
)
|
|
665
|
+
has_summary_json = (
|
|
666
|
+
'"summary"' in msg_content
|
|
667
|
+
and '"next_items"' in msg_content
|
|
668
|
+
) or (
|
|
669
|
+
"'summary'" in msg_content
|
|
670
|
+
and "'next_items'" in msg_content
|
|
671
|
+
)
|
|
672
|
+
if not has_summary_json:
|
|
673
|
+
# Just log warning - don't block completion to avoid infinite loop
|
|
674
|
+
logger.warning(
|
|
675
|
+
"Summary todo marked completed but no summary JSON in content. "
|
|
676
|
+
"Allowing completion to proceed. Content: %s",
|
|
677
|
+
msg_content[:200],
|
|
678
|
+
)
|
|
679
|
+
|
|
507
680
|
return response
|
|
508
681
|
|
|
509
682
|
return normalize_tool_args
|
|
@@ -543,16 +716,45 @@ def create_inject_continuation_middleware(wrap_model_call):
|
|
|
543
716
|
pass
|
|
544
717
|
|
|
545
718
|
if tool_name in NON_HITL_TOOLS:
|
|
546
|
-
|
|
547
|
-
"Injecting continuation prompt after non-HITL tool: %s",
|
|
548
|
-
tool_name,
|
|
549
|
-
)
|
|
550
|
-
|
|
719
|
+
# Method 1: Check state.todos
|
|
551
720
|
todos = request.state.get("todos", [])
|
|
552
721
|
pending_todos = [
|
|
553
722
|
t for t in todos if t.get("status") in ("pending", "in_progress")
|
|
554
723
|
]
|
|
555
724
|
|
|
725
|
+
# If all todos are completed, DON'T call LLM - return empty response to stop agent
|
|
726
|
+
if not pending_todos and todos:
|
|
727
|
+
logger.info(
|
|
728
|
+
"All todos completed (from state) after tool: %s - stopping agent (no LLM call)",
|
|
729
|
+
tool_name,
|
|
730
|
+
)
|
|
731
|
+
return AIMessage(content="", tool_calls=[])
|
|
732
|
+
|
|
733
|
+
# Method 2: Check ToolMessage content for write_todos
|
|
734
|
+
if tool_name == "write_todos" or "Updated todo list to" in (last_msg.content or ""):
|
|
735
|
+
try:
|
|
736
|
+
import ast
|
|
737
|
+
content = last_msg.content or ""
|
|
738
|
+
if "Updated todo list to" in content:
|
|
739
|
+
list_text = content.split("Updated todo list to", 1)[1].strip()
|
|
740
|
+
todos_from_msg = ast.literal_eval(list_text)
|
|
741
|
+
if isinstance(todos_from_msg, list) and len(todos_from_msg) > 0:
|
|
742
|
+
pending = [t for t in todos_from_msg if t.get("status") in ("pending", "in_progress")]
|
|
743
|
+
if not pending:
|
|
744
|
+
logger.info(
|
|
745
|
+
"All %d todos completed (from ToolMessage) after tool: %s - stopping agent",
|
|
746
|
+
len(todos_from_msg),
|
|
747
|
+
tool_name,
|
|
748
|
+
)
|
|
749
|
+
return AIMessage(content="", tool_calls=[])
|
|
750
|
+
except Exception as e:
|
|
751
|
+
logger.debug("Failed to parse todos from ToolMessage: %s", e)
|
|
752
|
+
|
|
753
|
+
logger.info(
|
|
754
|
+
"Injecting continuation prompt after non-HITL tool: %s",
|
|
755
|
+
tool_name,
|
|
756
|
+
)
|
|
757
|
+
|
|
556
758
|
if pending_todos:
|
|
557
759
|
pending_list = ", ".join(
|
|
558
760
|
t.get("content", "")[:30] for t in pending_todos[:3]
|
|
@@ -563,9 +765,10 @@ def create_inject_continuation_middleware(wrap_model_call):
|
|
|
563
765
|
f"Call jupyter_cell_tool or the next appropriate tool."
|
|
564
766
|
)
|
|
565
767
|
else:
|
|
768
|
+
# No todos yet - let agent create them
|
|
566
769
|
continuation = (
|
|
567
|
-
f"Tool '{tool_name}' completed.
|
|
568
|
-
f"
|
|
770
|
+
f"Tool '{tool_name}' completed. "
|
|
771
|
+
f"Create a todo list with write_todos if needed."
|
|
569
772
|
)
|
|
570
773
|
|
|
571
774
|
new_messages = list(messages) + [
|