hdsp-jupyter-extension 2.0.1__py3-none-any.whl → 2.0.2__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/langchain/__init__.py +18 -0
- agent_server/langchain/agent.py +694 -0
- agent_server/langchain/executors/__init__.py +15 -0
- agent_server/langchain/executors/jupyter_executor.py +429 -0
- agent_server/langchain/executors/notebook_searcher.py +477 -0
- agent_server/langchain/middleware/__init__.py +36 -0
- agent_server/langchain/middleware/code_search_middleware.py +278 -0
- agent_server/langchain/middleware/error_handling_middleware.py +338 -0
- agent_server/langchain/middleware/jupyter_execution_middleware.py +301 -0
- agent_server/langchain/middleware/rag_middleware.py +227 -0
- agent_server/langchain/middleware/validation_middleware.py +240 -0
- agent_server/langchain/state.py +159 -0
- agent_server/langchain/tools/__init__.py +39 -0
- agent_server/langchain/tools/file_tools.py +279 -0
- agent_server/langchain/tools/jupyter_tools.py +143 -0
- agent_server/langchain/tools/search_tools.py +309 -0
- agent_server/main.py +13 -0
- agent_server/routers/health.py +14 -0
- agent_server/routers/langchain_agent.py +1368 -0
- {hdsp_jupyter_extension-2.0.1.data → hdsp_jupyter_extension-2.0.2.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.1.data → hdsp_jupyter_extension-2.0.2.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
- hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2607ff74c74acfa83158.js → hdsp_jupyter_extension-2.0.2.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.634cf0ae0f3592d0882f.js +408 -4
- hdsp_jupyter_extension-2.0.2.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.634cf0ae0f3592d0882f.js.map +1 -0
- hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.622c1a5918b3aafb2315.js → hdsp_jupyter_extension-2.0.2.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.1366019c413f1d68467f.js +753 -65
- hdsp_jupyter_extension-2.0.2.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.1366019c413f1d68467f.js.map +1 -0
- hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.729f933de01ad5620730.js → hdsp_jupyter_extension-2.0.2.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.3379c4b222c042de2b01.js +8 -8
- hdsp_jupyter_extension-2.0.2.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.3379c4b222c042de2b01.js.map +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js → hdsp_jupyter_extension-2.0.2.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +2 -209
- hdsp_jupyter_extension-2.0.2.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js → hdsp_jupyter_extension-2.0.2.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +209 -2
- hdsp_jupyter_extension-2.0.2.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +1 -0
- hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → hdsp_jupyter_extension-2.0.2.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +212 -3
- hdsp_jupyter_extension-2.0.2.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +1 -0
- {hdsp_jupyter_extension-2.0.1.dist-info → hdsp_jupyter_extension-2.0.2.dist-info}/METADATA +1 -1
- {hdsp_jupyter_extension-2.0.1.dist-info → hdsp_jupyter_extension-2.0.2.dist-info}/RECORD +66 -49
- jupyter_ext/_version.py +1 -1
- jupyter_ext/handlers.py +126 -1
- jupyter_ext/labextension/build_log.json +1 -1
- jupyter_ext/labextension/package.json +2 -2
- jupyter_ext/labextension/static/{frontend_styles_index_js.2607ff74c74acfa83158.js → frontend_styles_index_js.634cf0ae0f3592d0882f.js} +408 -4
- jupyter_ext/labextension/static/frontend_styles_index_js.634cf0ae0f3592d0882f.js.map +1 -0
- jupyter_ext/labextension/static/{lib_index_js.622c1a5918b3aafb2315.js → lib_index_js.1366019c413f1d68467f.js} +753 -65
- jupyter_ext/labextension/static/lib_index_js.1366019c413f1d68467f.js.map +1 -0
- jupyter_ext/labextension/static/{remoteEntry.729f933de01ad5620730.js → remoteEntry.3379c4b222c042de2b01.js} +8 -8
- jupyter_ext/labextension/static/remoteEntry.3379c4b222c042de2b01.js.map +1 -0
- hdsp_jupyter_extension-2.0.1.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 → jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +2 -209
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +1 -0
- hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +209 -2
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +1 -0
- jupyter_ext/labextension/static/{vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js} +212 -3
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +1 -0
- hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2607ff74c74acfa83158.js.map +0 -1
- hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.622c1a5918b3aafb2315.js.map +0 -1
- hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.729f933de01ad5620730.js.map +0 -1
- hdsp_jupyter_extension-2.0.1.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 +0 -1
- hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
- hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
- jupyter_ext/labextension/static/frontend_styles_index_js.2607ff74c74acfa83158.js.map +0 -1
- jupyter_ext/labextension/static/lib_index_js.622c1a5918b3aafb2315.js.map +0 -1
- jupyter_ext/labextension/static/remoteEntry.729f933de01ad5620730.js.map +0 -1
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +0 -1
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
- {hdsp_jupyter_extension-2.0.1.data → hdsp_jupyter_extension-2.0.2.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
- {hdsp_jupyter_extension-2.0.1.data → hdsp_jupyter_extension-2.0.2.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.1.data → hdsp_jupyter_extension-2.0.2.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.1.data → hdsp_jupyter_extension-2.0.2.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.1.data → hdsp_jupyter_extension-2.0.2.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.1.data → hdsp_jupyter_extension-2.0.2.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.1.data → hdsp_jupyter_extension-2.0.2.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.1.data → hdsp_jupyter_extension-2.0.2.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.1.data → hdsp_jupyter_extension-2.0.2.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.1.data → hdsp_jupyter_extension-2.0.2.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.1.data → hdsp_jupyter_extension-2.0.2.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.1.data → hdsp_jupyter_extension-2.0.2.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
- {hdsp_jupyter_extension-2.0.1.data → hdsp_jupyter_extension-2.0.2.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.1.dist-info → hdsp_jupyter_extension-2.0.2.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.1.dist-info → hdsp_jupyter_extension-2.0.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangChain Agent
|
|
3
|
+
|
|
4
|
+
Main agent creation module for tool-driven chat execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from agent_server.langchain.tools import (
|
|
11
|
+
final_answer_tool,
|
|
12
|
+
jupyter_cell_tool,
|
|
13
|
+
list_files_tool,
|
|
14
|
+
markdown_tool,
|
|
15
|
+
read_file_tool,
|
|
16
|
+
search_notebook_cells_tool,
|
|
17
|
+
search_workspace_tool,
|
|
18
|
+
write_file_tool,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _create_llm(llm_config: Dict[str, Any]):
|
|
25
|
+
"""Create LangChain LLM from config"""
|
|
26
|
+
provider = llm_config.get("provider", "gemini")
|
|
27
|
+
|
|
28
|
+
if provider == "gemini":
|
|
29
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
30
|
+
|
|
31
|
+
gemini_config = llm_config.get("gemini", {})
|
|
32
|
+
api_key = gemini_config.get("apiKey")
|
|
33
|
+
model = gemini_config.get("model", "gemini-2.5-pro")
|
|
34
|
+
|
|
35
|
+
if not api_key:
|
|
36
|
+
raise ValueError("Gemini API key not configured")
|
|
37
|
+
|
|
38
|
+
logger.info(f"Creating Gemini LLM with model: {model}")
|
|
39
|
+
|
|
40
|
+
# Gemini 2.5 Flash has issues with tool calling in LangChain
|
|
41
|
+
# Use convert_system_message_to_human for better compatibility
|
|
42
|
+
llm = ChatGoogleGenerativeAI(
|
|
43
|
+
model=model,
|
|
44
|
+
google_api_key=api_key,
|
|
45
|
+
temperature=0.0,
|
|
46
|
+
max_output_tokens=8192,
|
|
47
|
+
convert_system_message_to_human=True, # Better tool calling support
|
|
48
|
+
)
|
|
49
|
+
return llm
|
|
50
|
+
|
|
51
|
+
elif provider == "openai":
|
|
52
|
+
from langchain_openai import ChatOpenAI
|
|
53
|
+
|
|
54
|
+
openai_config = llm_config.get("openai", {})
|
|
55
|
+
api_key = openai_config.get("apiKey")
|
|
56
|
+
model = openai_config.get("model", "gpt-4")
|
|
57
|
+
|
|
58
|
+
if not api_key:
|
|
59
|
+
raise ValueError("OpenAI API key not configured")
|
|
60
|
+
|
|
61
|
+
llm = ChatOpenAI(
|
|
62
|
+
model=model,
|
|
63
|
+
api_key=api_key,
|
|
64
|
+
temperature=0.0,
|
|
65
|
+
max_tokens=4096,
|
|
66
|
+
)
|
|
67
|
+
return llm
|
|
68
|
+
|
|
69
|
+
elif provider == "vllm":
|
|
70
|
+
from langchain_openai import ChatOpenAI
|
|
71
|
+
|
|
72
|
+
vllm_config = llm_config.get("vllm", {})
|
|
73
|
+
endpoint = vllm_config.get("endpoint", "http://localhost:8000")
|
|
74
|
+
model = vllm_config.get("model", "default")
|
|
75
|
+
api_key = vllm_config.get("apiKey", "dummy")
|
|
76
|
+
|
|
77
|
+
llm = ChatOpenAI(
|
|
78
|
+
model=model,
|
|
79
|
+
api_key=api_key,
|
|
80
|
+
base_url=f"{endpoint}/v1",
|
|
81
|
+
temperature=0.0,
|
|
82
|
+
max_tokens=4096,
|
|
83
|
+
)
|
|
84
|
+
return llm
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError(f"Unsupported LLM provider: {provider}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_all_tools():
|
|
91
|
+
"""Get all available tools for the agent"""
|
|
92
|
+
return [
|
|
93
|
+
jupyter_cell_tool,
|
|
94
|
+
markdown_tool,
|
|
95
|
+
final_answer_tool,
|
|
96
|
+
read_file_tool,
|
|
97
|
+
write_file_tool,
|
|
98
|
+
list_files_tool,
|
|
99
|
+
search_workspace_tool,
|
|
100
|
+
search_notebook_cells_tool,
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def create_simple_chat_agent(
|
|
105
|
+
llm_config: Dict[str, Any],
|
|
106
|
+
workspace_root: str = ".",
|
|
107
|
+
enable_hitl: bool = True,
|
|
108
|
+
enable_todo_list: bool = True,
|
|
109
|
+
checkpointer: Optional[object] = None,
|
|
110
|
+
):
|
|
111
|
+
"""
|
|
112
|
+
Create a simple chat agent using LangChain's create_agent with Human-in-the-Loop.
|
|
113
|
+
|
|
114
|
+
This is a simplified version for chat mode that uses LangChain's built-in
|
|
115
|
+
HumanInTheLoopMiddleware and TodoListMiddleware.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
llm_config: LLM configuration
|
|
119
|
+
workspace_root: Root directory
|
|
120
|
+
enable_hitl: Enable Human-in-the-Loop for code execution
|
|
121
|
+
enable_todo_list: Enable TodoListMiddleware for task planning
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Configured agent with HITL and TodoList middleware
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
from langchain.agents import create_agent
|
|
128
|
+
from langchain.agents.middleware import (
|
|
129
|
+
AgentMiddleware,
|
|
130
|
+
HumanInTheLoopMiddleware,
|
|
131
|
+
ModelCallLimitMiddleware,
|
|
132
|
+
ModelRequest,
|
|
133
|
+
ModelResponse,
|
|
134
|
+
TodoListMiddleware,
|
|
135
|
+
ToolCallLimitMiddleware,
|
|
136
|
+
wrap_model_call,
|
|
137
|
+
)
|
|
138
|
+
from langchain_core.messages import AIMessage, ToolMessage as LCToolMessage
|
|
139
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
140
|
+
from langgraph.types import Overwrite
|
|
141
|
+
except ImportError as e:
|
|
142
|
+
logger.error(f"Failed to import LangChain agent components: {e}")
|
|
143
|
+
raise ImportError(
|
|
144
|
+
"LangChain agent components not available. "
|
|
145
|
+
"Install with: pip install langchain langgraph"
|
|
146
|
+
) from e
|
|
147
|
+
|
|
148
|
+
# Create LLM
|
|
149
|
+
llm = _create_llm(llm_config)
|
|
150
|
+
|
|
151
|
+
# Get tools
|
|
152
|
+
tools = _get_all_tools()
|
|
153
|
+
|
|
154
|
+
# Configure middleware
|
|
155
|
+
middleware = []
|
|
156
|
+
|
|
157
|
+
# JSON Schema for fallback tool calling
|
|
158
|
+
JSON_TOOL_SCHEMA = """You MUST respond with ONLY valid JSON matching this schema:
|
|
159
|
+
{
|
|
160
|
+
"tool": "<tool_name>",
|
|
161
|
+
"arguments": {"arg1": "value1", ...}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
Available tools:
|
|
165
|
+
- jupyter_cell_tool: Execute Python code. Arguments: {"code": "<python_code>"}
|
|
166
|
+
- markdown_tool: Add markdown cell. Arguments: {"content": "<markdown>"}
|
|
167
|
+
- final_answer_tool: Complete task. Arguments: {"answer": "<summary>"}
|
|
168
|
+
- write_todos: Update task list. Arguments: {"todos": [{"content": "...", "status": "pending|in_progress|completed"}]}
|
|
169
|
+
- read_file_tool: Read file. Arguments: {"path": "<file_path>"}
|
|
170
|
+
- list_files_tool: List directory. Arguments: {"path": "."}
|
|
171
|
+
|
|
172
|
+
Output ONLY the JSON object, no markdown, no explanation."""
|
|
173
|
+
|
|
174
|
+
def _parse_json_tool_call(text: str) -> Optional[Dict[str, Any]]:
|
|
175
|
+
"""Parse JSON tool call from text response."""
|
|
176
|
+
import json
|
|
177
|
+
import re
|
|
178
|
+
|
|
179
|
+
if not text:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
# Clean up response
|
|
183
|
+
text = text.strip()
|
|
184
|
+
if text.startswith("```json"):
|
|
185
|
+
text = text[7:]
|
|
186
|
+
elif text.startswith("```"):
|
|
187
|
+
text = text[3:]
|
|
188
|
+
if text.endswith("```"):
|
|
189
|
+
text = text[:-3]
|
|
190
|
+
text = text.strip()
|
|
191
|
+
|
|
192
|
+
# Try direct JSON parse
|
|
193
|
+
try:
|
|
194
|
+
data = json.loads(text)
|
|
195
|
+
if "tool" in data:
|
|
196
|
+
return data
|
|
197
|
+
except json.JSONDecodeError:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
# Try to find JSON object in response
|
|
201
|
+
json_match = re.search(r'\{[\s\S]*\}', text)
|
|
202
|
+
if json_match:
|
|
203
|
+
try:
|
|
204
|
+
data = json.loads(json_match.group())
|
|
205
|
+
if "tool" in data:
|
|
206
|
+
return data
|
|
207
|
+
except json.JSONDecodeError:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
def _create_tool_call_message(tool_name: str, arguments: Dict[str, Any]) -> AIMessage:
|
|
213
|
+
"""Create AIMessage with tool_calls from parsed JSON."""
|
|
214
|
+
import uuid
|
|
215
|
+
|
|
216
|
+
# Normalize tool name
|
|
217
|
+
if not tool_name.endswith("_tool"):
|
|
218
|
+
tool_name = f"{tool_name}_tool"
|
|
219
|
+
|
|
220
|
+
return AIMessage(
|
|
221
|
+
content="",
|
|
222
|
+
tool_calls=[
|
|
223
|
+
{
|
|
224
|
+
"name": tool_name,
|
|
225
|
+
"args": arguments,
|
|
226
|
+
"id": str(uuid.uuid4()),
|
|
227
|
+
"type": "tool_call",
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Middleware to detect and handle empty LLM responses with JSON fallback
|
|
233
|
+
@wrap_model_call
|
|
234
|
+
def handle_empty_response(
|
|
235
|
+
request: ModelRequest,
|
|
236
|
+
handler,
|
|
237
|
+
) -> ModelResponse:
|
|
238
|
+
"""
|
|
239
|
+
Detect empty/invalid AIMessage responses and retry with JSON schema fallback.
|
|
240
|
+
|
|
241
|
+
For models that don't support native tool calling well (e.g., Gemini 2.5 Flash),
|
|
242
|
+
this middleware:
|
|
243
|
+
1. Detects empty or text-only responses (no tool_calls)
|
|
244
|
+
2. Retries with JSON schema prompt to force structured output
|
|
245
|
+
3. Parses JSON response and injects tool_calls into AIMessage
|
|
246
|
+
4. Falls back to synthetic final_answer if all else fails
|
|
247
|
+
"""
|
|
248
|
+
import json
|
|
249
|
+
import uuid
|
|
250
|
+
from langchain_core.messages import HumanMessage
|
|
251
|
+
|
|
252
|
+
max_retries = 2 # Allow more retries for JSON fallback
|
|
253
|
+
|
|
254
|
+
for attempt in range(max_retries + 1):
|
|
255
|
+
response = handler(request)
|
|
256
|
+
|
|
257
|
+
# Extract AIMessage from response
|
|
258
|
+
response_message = None
|
|
259
|
+
if hasattr(response, 'result'):
|
|
260
|
+
result = response.result
|
|
261
|
+
if isinstance(result, list):
|
|
262
|
+
for msg in reversed(result):
|
|
263
|
+
if isinstance(msg, AIMessage):
|
|
264
|
+
response_message = msg
|
|
265
|
+
break
|
|
266
|
+
elif isinstance(result, AIMessage):
|
|
267
|
+
response_message = result
|
|
268
|
+
elif hasattr(response, 'message'):
|
|
269
|
+
response_message = response.message
|
|
270
|
+
elif hasattr(response, 'messages') and response.messages:
|
|
271
|
+
response_message = response.messages[-1]
|
|
272
|
+
elif isinstance(response, AIMessage):
|
|
273
|
+
response_message = response
|
|
274
|
+
|
|
275
|
+
has_content = bool(getattr(response_message, 'content', None)) if response_message else False
|
|
276
|
+
has_tool_calls = bool(getattr(response_message, 'tool_calls', None)) if response_message else False
|
|
277
|
+
|
|
278
|
+
logger.info(
|
|
279
|
+
"handle_empty_response: attempt=%d, type=%s, content=%s, tool_calls=%s",
|
|
280
|
+
attempt + 1,
|
|
281
|
+
type(response_message).__name__ if response_message else None,
|
|
282
|
+
has_content,
|
|
283
|
+
has_tool_calls,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Valid response with tool_calls
|
|
287
|
+
if has_tool_calls:
|
|
288
|
+
return response
|
|
289
|
+
|
|
290
|
+
# Try to parse JSON from content (model might have output JSON without tool_calls)
|
|
291
|
+
if has_content and response_message:
|
|
292
|
+
parsed = _parse_json_tool_call(response_message.content)
|
|
293
|
+
if parsed:
|
|
294
|
+
tool_name = parsed.get("tool", "")
|
|
295
|
+
arguments = parsed.get("arguments", {})
|
|
296
|
+
logger.info(
|
|
297
|
+
"Parsed JSON tool call from content: tool=%s",
|
|
298
|
+
tool_name,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Create new AIMessage with tool_calls
|
|
302
|
+
new_message = _create_tool_call_message(tool_name, arguments)
|
|
303
|
+
|
|
304
|
+
# Replace in response
|
|
305
|
+
if hasattr(response, 'result'):
|
|
306
|
+
if isinstance(response.result, list):
|
|
307
|
+
new_result = [
|
|
308
|
+
new_message if isinstance(m, AIMessage) else m
|
|
309
|
+
for m in response.result
|
|
310
|
+
]
|
|
311
|
+
response.result = new_result
|
|
312
|
+
else:
|
|
313
|
+
response.result = new_message
|
|
314
|
+
return response
|
|
315
|
+
|
|
316
|
+
# Invalid response - retry with JSON schema prompt
|
|
317
|
+
if response_message and attempt < max_retries:
|
|
318
|
+
reason = "text-only" if has_content else "empty"
|
|
319
|
+
logger.warning(
|
|
320
|
+
"Invalid AIMessage (%s) detected (attempt %d/%d). "
|
|
321
|
+
"Retrying with JSON schema prompt...",
|
|
322
|
+
reason,
|
|
323
|
+
attempt + 1,
|
|
324
|
+
max_retries + 1,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Get context for prompt
|
|
328
|
+
todos = request.state.get("todos", [])
|
|
329
|
+
pending_todos = [
|
|
330
|
+
t for t in todos
|
|
331
|
+
if t.get("status") in ("pending", "in_progress")
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
# Build JSON-forcing prompt
|
|
335
|
+
if has_content:
|
|
336
|
+
# LLM wrote text - ask to wrap in final_answer
|
|
337
|
+
content_preview = response_message.content[:300]
|
|
338
|
+
json_prompt = (
|
|
339
|
+
f"{JSON_TOOL_SCHEMA}\n\n"
|
|
340
|
+
f"Your previous response was text, not JSON. "
|
|
341
|
+
f"Wrap your answer in final_answer_tool:\n"
|
|
342
|
+
f'{{"tool": "final_answer_tool", "arguments": {{"answer": "{content_preview}..."}}}}'
|
|
343
|
+
)
|
|
344
|
+
elif pending_todos:
|
|
345
|
+
todo_list = ", ".join(t.get("content", "")[:20] for t in pending_todos[:3])
|
|
346
|
+
example_json = '{"tool": "jupyter_cell_tool", "arguments": {"code": "import pandas as pd\\ndf = pd.read_csv(\'titanic.csv\')\\nprint(df.head())"}}'
|
|
347
|
+
json_prompt = (
|
|
348
|
+
f"{JSON_TOOL_SCHEMA}\n\n"
|
|
349
|
+
f"Pending tasks: {todo_list}\n"
|
|
350
|
+
f"Call jupyter_cell_tool with Python code to complete the next task.\n"
|
|
351
|
+
f"Example: {example_json}"
|
|
352
|
+
)
|
|
353
|
+
else:
|
|
354
|
+
json_prompt = (
|
|
355
|
+
f"{JSON_TOOL_SCHEMA}\n\n"
|
|
356
|
+
f"All tasks completed. Call final_answer_tool:\n"
|
|
357
|
+
f'{{"tool": "final_answer_tool", "arguments": {{"answer": "작업이 완료되었습니다."}}}}'
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Add JSON prompt and retry
|
|
361
|
+
request = request.override(
|
|
362
|
+
messages=request.messages + [
|
|
363
|
+
HumanMessage(content=json_prompt)
|
|
364
|
+
]
|
|
365
|
+
)
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
# Max retries exhausted - synthesize final_answer
|
|
369
|
+
if response_message:
|
|
370
|
+
logger.warning(
|
|
371
|
+
"Max retries exhausted. Synthesizing final_answer response."
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Use LLM's text content if available
|
|
375
|
+
if has_content and response_message.content:
|
|
376
|
+
summary = response_message.content
|
|
377
|
+
logger.info(
|
|
378
|
+
"Using LLM's text content as final answer (length=%d)",
|
|
379
|
+
len(summary),
|
|
380
|
+
)
|
|
381
|
+
else:
|
|
382
|
+
todos = request.state.get("todos", [])
|
|
383
|
+
completed_todos = [
|
|
384
|
+
t.get("content", "") for t in todos
|
|
385
|
+
if t.get("status") == "completed"
|
|
386
|
+
]
|
|
387
|
+
summary = (
|
|
388
|
+
f"작업이 완료되었습니다. 완료된 항목: {', '.join(completed_todos[:5])}"
|
|
389
|
+
if completed_todos
|
|
390
|
+
else "작업이 완료되었습니다."
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Create synthetic final_answer
|
|
394
|
+
synthetic_message = AIMessage(
|
|
395
|
+
content="",
|
|
396
|
+
tool_calls=[
|
|
397
|
+
{
|
|
398
|
+
"name": "final_answer_tool",
|
|
399
|
+
"args": {"answer": summary},
|
|
400
|
+
"id": str(uuid.uuid4()),
|
|
401
|
+
"type": "tool_call",
|
|
402
|
+
}
|
|
403
|
+
],
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Replace in response
|
|
407
|
+
if hasattr(response, 'result'):
|
|
408
|
+
if isinstance(response.result, list):
|
|
409
|
+
new_result = []
|
|
410
|
+
replaced = False
|
|
411
|
+
for msg in response.result:
|
|
412
|
+
if isinstance(msg, AIMessage) and not replaced:
|
|
413
|
+
new_result.append(synthetic_message)
|
|
414
|
+
replaced = True
|
|
415
|
+
else:
|
|
416
|
+
new_result.append(msg)
|
|
417
|
+
if not replaced:
|
|
418
|
+
new_result.append(synthetic_message)
|
|
419
|
+
response.result = new_result
|
|
420
|
+
else:
|
|
421
|
+
response.result = synthetic_message
|
|
422
|
+
|
|
423
|
+
return response
|
|
424
|
+
|
|
425
|
+
# Return response (either valid or after max retries)
|
|
426
|
+
return response
|
|
427
|
+
|
|
428
|
+
return response
|
|
429
|
+
|
|
430
|
+
middleware.append(handle_empty_response)
|
|
431
|
+
|
|
432
|
+
# Non-HITL tools that execute immediately without user approval
|
|
433
|
+
NON_HITL_TOOLS = {
|
|
434
|
+
"markdown_tool", "markdown",
|
|
435
|
+
"read_file_tool", "read_file",
|
|
436
|
+
"list_files_tool", "list_files",
|
|
437
|
+
"search_workspace_tool", "search_workspace",
|
|
438
|
+
"search_notebook_cells_tool", "search_notebook_cells",
|
|
439
|
+
"write_todos",
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
# Middleware to inject continuation prompt after non-HITL tool execution
|
|
443
|
+
@wrap_model_call
|
|
444
|
+
def inject_continuation_after_non_hitl_tool(
|
|
445
|
+
request: ModelRequest,
|
|
446
|
+
handler,
|
|
447
|
+
) -> ModelResponse:
|
|
448
|
+
"""
|
|
449
|
+
Inject a continuation prompt when the last message is from a non-HITL tool.
|
|
450
|
+
|
|
451
|
+
Non-HITL tools execute immediately without user approval, which can cause
|
|
452
|
+
Gemini to produce empty responses. This middleware injects a system message
|
|
453
|
+
to remind the LLM to continue with the next action.
|
|
454
|
+
"""
|
|
455
|
+
messages = request.messages
|
|
456
|
+
if not messages:
|
|
457
|
+
return handler(request)
|
|
458
|
+
|
|
459
|
+
# Check if the last message is a ToolMessage from a non-HITL tool
|
|
460
|
+
last_msg = messages[-1]
|
|
461
|
+
if getattr(last_msg, "type", "") == "tool":
|
|
462
|
+
tool_name = getattr(last_msg, "name", "") or ""
|
|
463
|
+
|
|
464
|
+
# Also try to extract tool name from content
|
|
465
|
+
if not tool_name:
|
|
466
|
+
try:
|
|
467
|
+
import json
|
|
468
|
+
content_json = json.loads(last_msg.content)
|
|
469
|
+
tool_name = content_json.get("tool", "")
|
|
470
|
+
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
471
|
+
pass
|
|
472
|
+
|
|
473
|
+
if tool_name in NON_HITL_TOOLS:
|
|
474
|
+
logger.info(
|
|
475
|
+
"Injecting continuation prompt after non-HITL tool: %s",
|
|
476
|
+
tool_name,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Get todos context
|
|
480
|
+
todos = request.state.get("todos", [])
|
|
481
|
+
pending_todos = [
|
|
482
|
+
t for t in todos
|
|
483
|
+
if t.get("status") in ("pending", "in_progress")
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
if pending_todos:
|
|
487
|
+
pending_list = ", ".join(
|
|
488
|
+
t.get("content", "")[:30] for t in pending_todos[:3]
|
|
489
|
+
)
|
|
490
|
+
continuation = (
|
|
491
|
+
f"Tool '{tool_name}' completed. "
|
|
492
|
+
f"Continue with pending tasks: {pending_list}. "
|
|
493
|
+
f"Call jupyter_cell_tool or the next appropriate tool."
|
|
494
|
+
)
|
|
495
|
+
else:
|
|
496
|
+
continuation = (
|
|
497
|
+
f"Tool '{tool_name}' completed. All tasks done. "
|
|
498
|
+
f"Call final_answer_tool with a summary NOW."
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Inject as a system-like user message
|
|
502
|
+
from langchain_core.messages import HumanMessage
|
|
503
|
+
new_messages = list(messages) + [
|
|
504
|
+
HumanMessage(content=f"[SYSTEM] {continuation}")
|
|
505
|
+
]
|
|
506
|
+
request = request.override(messages=new_messages)
|
|
507
|
+
|
|
508
|
+
return handler(request)
|
|
509
|
+
|
|
510
|
+
middleware.append(inject_continuation_after_non_hitl_tool)
|
|
511
|
+
|
|
512
|
+
class PatchToolCallsMiddleware(AgentMiddleware):
|
|
513
|
+
"""Patch dangling tool calls so the agent can continue."""
|
|
514
|
+
|
|
515
|
+
def before_agent(self, state, runtime):
|
|
516
|
+
messages = state.get("messages", [])
|
|
517
|
+
if not messages:
|
|
518
|
+
return None
|
|
519
|
+
|
|
520
|
+
patched = []
|
|
521
|
+
for i, msg in enumerate(messages):
|
|
522
|
+
patched.append(msg)
|
|
523
|
+
if getattr(msg, "type", "") == "ai" and getattr(
|
|
524
|
+
msg, "tool_calls", None
|
|
525
|
+
):
|
|
526
|
+
for tool_call in msg.tool_calls:
|
|
527
|
+
tool_call_id = tool_call.get("id")
|
|
528
|
+
if not tool_call_id:
|
|
529
|
+
continue
|
|
530
|
+
has_tool_msg = any(
|
|
531
|
+
(
|
|
532
|
+
getattr(m, "type", "") == "tool"
|
|
533
|
+
and getattr(m, "tool_call_id", None) == tool_call_id
|
|
534
|
+
)
|
|
535
|
+
for m in messages[i:]
|
|
536
|
+
)
|
|
537
|
+
if not has_tool_msg:
|
|
538
|
+
tool_msg = (
|
|
539
|
+
f"Tool call {tool_call.get('name', 'unknown')} with id {tool_call_id} "
|
|
540
|
+
"was cancelled - another message came in before it could be completed."
|
|
541
|
+
)
|
|
542
|
+
patched.append(
|
|
543
|
+
LCToolMessage(
|
|
544
|
+
content=tool_msg,
|
|
545
|
+
name=tool_call.get("name", "unknown"),
|
|
546
|
+
tool_call_id=tool_call_id,
|
|
547
|
+
)
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if patched == messages:
|
|
551
|
+
return None
|
|
552
|
+
return {"messages": Overwrite(patched)}
|
|
553
|
+
|
|
554
|
+
middleware.append(PatchToolCallsMiddleware())
|
|
555
|
+
|
|
556
|
+
# Add TodoListMiddleware for task planning
|
|
557
|
+
if enable_todo_list:
|
|
558
|
+
todo_middleware = TodoListMiddleware(
|
|
559
|
+
system_prompt="""
|
|
560
|
+
## CRITICAL WORKFLOW RULES - MUST FOLLOW:
|
|
561
|
+
1. NEVER stop after calling write_todos - ALWAYS make another tool call immediately
|
|
562
|
+
2. write_todos is ONLY for tracking progress - it does NOT complete any work
|
|
563
|
+
3. After EVERY write_todos call, you MUST call another tool (jupyter_cell_tool, markdown_tool, or final_answer_tool)
|
|
564
|
+
|
|
565
|
+
## Todo List Management:
|
|
566
|
+
- Before complex tasks, use write_todos to create a task list
|
|
567
|
+
- Update todos as you complete each step (mark 'in_progress' → 'completed')
|
|
568
|
+
- Each todo item should be specific and descriptive (10-50 characters)
|
|
569
|
+
- All todo items must be written in Korean
|
|
570
|
+
- ALWAYS include "다음 단계 제시" as the LAST item
|
|
571
|
+
|
|
572
|
+
## Task Completion Flow:
|
|
573
|
+
1. When current task is done → mark it 'completed' with write_todos
|
|
574
|
+
2. IMMEDIATELY call the next tool (jupyter_cell_tool for code, markdown_tool for text)
|
|
575
|
+
3. For "다음 단계 제시" → mark completed, then call final_answer_tool with suggestions
|
|
576
|
+
4. NEVER end your turn after write_todos - you MUST continue with actual work
|
|
577
|
+
|
|
578
|
+
## FORBIDDEN PATTERNS:
|
|
579
|
+
❌ Calling write_todos and then stopping
|
|
580
|
+
❌ Updating todo status without doing the actual work
|
|
581
|
+
❌ Ending turn without calling final_answer_tool when all tasks are done
|
|
582
|
+
""",
|
|
583
|
+
tool_description="""Update the task list for tracking progress.
|
|
584
|
+
⚠️ CRITICAL: This tool is ONLY for tracking - it does NOT do any actual work.
|
|
585
|
+
After calling this tool, you MUST IMMEDIATELY call another tool (jupyter_cell_tool, markdown_tool, or final_answer_tool).
|
|
586
|
+
NEVER end your response after calling write_todos - always continue with the next action tool.""",
|
|
587
|
+
)
|
|
588
|
+
middleware.append(todo_middleware)
|
|
589
|
+
|
|
590
|
+
if enable_hitl:
|
|
591
|
+
# Add Human-in-the-Loop middleware for code execution
|
|
592
|
+
hitl_middleware = HumanInTheLoopMiddleware(
|
|
593
|
+
interrupt_on={
|
|
594
|
+
# Require approval before executing code
|
|
595
|
+
"jupyter_cell_tool": {
|
|
596
|
+
"allowed_decisions": ["approve", "edit", "reject"],
|
|
597
|
+
"description": "🔍 Code execution requires approval",
|
|
598
|
+
},
|
|
599
|
+
# Safe operations - no approval needed
|
|
600
|
+
"markdown_tool": False,
|
|
601
|
+
"read_file_tool": False,
|
|
602
|
+
"list_files_tool": False,
|
|
603
|
+
"search_workspace_tool": False,
|
|
604
|
+
"search_notebook_cells_tool": False,
|
|
605
|
+
"write_todos": False, # Todo updates don't need approval
|
|
606
|
+
# File write requires approval
|
|
607
|
+
"write_file_tool": {
|
|
608
|
+
"allowed_decisions": ["approve", "edit", "reject"],
|
|
609
|
+
"description": "⚠️ File write requires approval",
|
|
610
|
+
},
|
|
611
|
+
# Final answer doesn't need approval
|
|
612
|
+
"final_answer_tool": False,
|
|
613
|
+
},
|
|
614
|
+
description_prefix="Tool execution pending approval",
|
|
615
|
+
)
|
|
616
|
+
middleware.append(hitl_middleware)
|
|
617
|
+
|
|
618
|
+
# Add loop prevention middleware
|
|
619
|
+
# ModelCallLimitMiddleware: Prevent infinite LLM calls
|
|
620
|
+
model_limit_middleware = ModelCallLimitMiddleware(
|
|
621
|
+
run_limit=30, # Max 30 LLM calls per user message
|
|
622
|
+
exit_behavior="end", # Gracefully end when limit reached
|
|
623
|
+
)
|
|
624
|
+
middleware.append(model_limit_middleware)
|
|
625
|
+
logger.info("Added ModelCallLimitMiddleware with run_limit=30")
|
|
626
|
+
|
|
627
|
+
# ToolCallLimitMiddleware: Prevent specific tools from being called too many times
|
|
628
|
+
# Limit write_todos to prevent the loop we observed
|
|
629
|
+
write_todos_limit = ToolCallLimitMiddleware(
|
|
630
|
+
tool_name="write_todos",
|
|
631
|
+
run_limit=5, # Max 5 write_todos calls per user message
|
|
632
|
+
exit_behavior="continue", # Let agent continue with other tools
|
|
633
|
+
)
|
|
634
|
+
middleware.append(write_todos_limit)
|
|
635
|
+
|
|
636
|
+
# Limit list_files_tool to prevent excessive directory listing
|
|
637
|
+
list_files_limit = ToolCallLimitMiddleware(
|
|
638
|
+
tool_name="list_files_tool",
|
|
639
|
+
run_limit=5, # Max 5 list_files calls per user message
|
|
640
|
+
exit_behavior="continue",
|
|
641
|
+
)
|
|
642
|
+
middleware.append(list_files_limit)
|
|
643
|
+
logger.info("Added ToolCallLimitMiddleware for write_todos and list_files_tool")
|
|
644
|
+
|
|
645
|
+
# System prompt for the agent
|
|
646
|
+
system_prompt = """You are an expert Python data scientist and Jupyter notebook assistant.
|
|
647
|
+
Your role is to help users with data analysis, visualization, and Python coding tasks in Jupyter notebooks.
|
|
648
|
+
|
|
649
|
+
## ⚠️ CRITICAL RULE: NEVER produce an empty response
|
|
650
|
+
|
|
651
|
+
You MUST ALWAYS call a tool in every response. After any tool result, you MUST:
|
|
652
|
+
1. Check your todo list - are there pending or in_progress items?
|
|
653
|
+
2. If YES → call the next appropriate tool (jupyter_cell_tool, markdown_tool, etc.)
|
|
654
|
+
3. If ALL todos are completed → call final_answer_tool with a summary
|
|
655
|
+
|
|
656
|
+
NEVER end your turn without calling a tool. NEVER produce an empty response.
|
|
657
|
+
|
|
658
|
+
## Available Tools
|
|
659
|
+
1. **jupyter_cell_tool**: Execute Python code in a new notebook cell
|
|
660
|
+
2. **markdown_tool**: Add a markdown explanation cell
|
|
661
|
+
3. **final_answer_tool**: Complete the task with a summary - REQUIRED when done
|
|
662
|
+
4. **read_file_tool**: Read file contents
|
|
663
|
+
5. **write_file_tool**: Write file contents
|
|
664
|
+
6. **list_files_tool**: List directory contents
|
|
665
|
+
7. **search_workspace_tool**: Search for patterns in workspace files
|
|
666
|
+
8. **search_notebook_cells_tool**: Search for patterns in notebook cells
|
|
667
|
+
9. **write_todos**: Create and update task list for complex multi-step tasks
|
|
668
|
+
|
|
669
|
+
## Mandatory Workflow
|
|
670
|
+
1. After EVERY tool result, immediately call the next tool
|
|
671
|
+
2. Continue until ALL todos show status: "completed"
|
|
672
|
+
3. ONLY THEN call final_answer_tool to summarize
|
|
673
|
+
4. If `!pip install` fails, use `!pip3 install` instead
|
|
674
|
+
5. For plots and charts, use English text only
|
|
675
|
+
|
|
676
|
+
## ❌ FORBIDDEN (will break the workflow)
|
|
677
|
+
- Producing an empty response (no tool call, no content)
|
|
678
|
+
- Stopping after any tool without calling the next tool
|
|
679
|
+
- Ending without calling final_answer_tool
|
|
680
|
+
- Leaving todos in "in_progress" or "pending" state without continuing
|
|
681
|
+
"""
|
|
682
|
+
|
|
683
|
+
logger.info("SimpleChatAgent system_prompt: %s", system_prompt)
|
|
684
|
+
|
|
685
|
+
# Create agent with checkpointer (required for HITL)
|
|
686
|
+
agent = create_agent(
|
|
687
|
+
model=llm,
|
|
688
|
+
tools=tools,
|
|
689
|
+
middleware=middleware,
|
|
690
|
+
checkpointer=checkpointer or InMemorySaver(), # Required for interrupt/resume
|
|
691
|
+
system_prompt=system_prompt, # Tell the agent to use tools
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
return agent
|