hdsp-jupyter-extension 2.0.7__py3-none-any.whl → 2.0.10__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/embedding_service.py +67 -46
- agent_server/core/rag_manager.py +40 -17
- agent_server/core/retriever.py +12 -6
- agent_server/core/vllm_embedding_service.py +246 -0
- agent_server/langchain/ARCHITECTURE.md +7 -51
- agent_server/langchain/agent.py +39 -20
- agent_server/langchain/custom_middleware.py +206 -62
- agent_server/langchain/hitl_config.py +6 -9
- agent_server/langchain/llm_factory.py +85 -1
- agent_server/langchain/logging_utils.py +52 -13
- agent_server/langchain/prompts.py +85 -45
- agent_server/langchain/tools/__init__.py +14 -10
- agent_server/langchain/tools/file_tools.py +266 -40
- agent_server/langchain/tools/file_utils.py +334 -0
- agent_server/langchain/tools/jupyter_tools.py +0 -1
- agent_server/langchain/tools/lsp_tools.py +264 -0
- agent_server/langchain/tools/resource_tools.py +12 -12
- agent_server/langchain/tools/search_tools.py +3 -158
- agent_server/main.py +7 -0
- agent_server/routers/langchain_agent.py +207 -102
- agent_server/routers/rag.py +8 -3
- hdsp_agent_core/models/rag.py +15 -1
- hdsp_agent_core/services/rag_service.py +6 -1
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +3 -2
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js +251 -5
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +1 -0
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js +1831 -274
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js.map +1 -0
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4a252df3ade74efee8d6.js +11 -9
- hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4a252df3ade74efee8d6.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.10.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.10.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.10.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.10.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
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → hdsp_jupyter_extension-2.0.10.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.10.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.7.dist-info → hdsp_jupyter_extension-2.0.10.dist-info}/METADATA +1 -3
- hdsp_jupyter_extension-2.0.10.dist-info/RECORD +144 -0
- jupyter_ext/__init__.py +18 -0
- jupyter_ext/_version.py +1 -1
- jupyter_ext/handlers.py +176 -1
- jupyter_ext/labextension/build_log.json +1 -1
- jupyter_ext/labextension/package.json +3 -2
- jupyter_ext/labextension/static/{frontend_styles_index_js.4770ec0fb2d173b6deb4.js → frontend_styles_index_js.2d9fb488c82498c45c2d.js} +251 -5
- jupyter_ext/labextension/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +1 -0
- jupyter_ext/labextension/static/{lib_index_js.29cf4312af19e86f82af.js → lib_index_js.dc6434bee96ab03a0539.js} +1831 -274
- jupyter_ext/labextension/static/lib_index_js.dc6434bee96ab03a0539.js.map +1 -0
- jupyter_ext/labextension/static/{remoteEntry.61343eb4cf0577e74b50.js → remoteEntry.4a252df3ade74efee8d6.js} +11 -9
- jupyter_ext/labextension/static/remoteEntry.4a252df3ade74efee8d6.js.map +1 -0
- 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 → 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.7.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
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → jupyter_ext/labextension/static/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.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js.map +0 -1
- 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 +0 -1
- 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 +0 -1
- 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 +0 -1
- hdsp_jupyter_extension-2.0.7.dist-info/RECORD +0 -141
- jupyter_ext/labextension/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +0 -1
- jupyter_ext/labextension/static/lib_index_js.29cf4312af19e86f82af.js.map +0 -1
- jupyter_ext/labextension/static/remoteEntry.61343eb4cf0577e74b50.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.7.data → hdsp_jupyter_extension-2.0.10.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.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.7.data → hdsp_jupyter_extension-2.0.10.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.7.data → hdsp_jupyter_extension-2.0.10.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.7.data → hdsp_jupyter_extension-2.0.10.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.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.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.7.data → hdsp_jupyter_extension-2.0.10.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.7.data → hdsp_jupyter_extension-2.0.10.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.7.data → hdsp_jupyter_extension-2.0.10.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.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.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.7.dist-info → hdsp_jupyter_extension-2.0.10.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Search Tools for LangChain Agent
|
|
3
3
|
|
|
4
|
-
Provides tools for searching
|
|
5
|
-
|
|
6
|
-
using subprocess (find/grep/ripgrep).
|
|
4
|
+
Provides tools for searching notebook cells.
|
|
5
|
+
For file searching, use execute_command_tool with find/grep commands.
|
|
7
6
|
|
|
8
7
|
Key features:
|
|
9
8
|
- Returns command info for client-side execution via subprocess
|
|
10
|
-
- Supports ripgrep (rg) if available, falls back to grep
|
|
11
9
|
- Executes immediately without user approval
|
|
12
10
|
- Shows the command being executed in status messages
|
|
13
11
|
"""
|
|
14
12
|
|
|
15
13
|
import logging
|
|
16
|
-
import shutil
|
|
17
14
|
from typing import Any, Dict, List, Optional
|
|
18
15
|
|
|
19
16
|
from langchain_core.tools import tool
|
|
@@ -22,23 +19,6 @@ from pydantic import BaseModel, Field
|
|
|
22
19
|
logger = logging.getLogger(__name__)
|
|
23
20
|
|
|
24
21
|
|
|
25
|
-
class SearchWorkspaceInput(BaseModel):
|
|
26
|
-
"""Input schema for search_workspace tool"""
|
|
27
|
-
|
|
28
|
-
pattern: str = Field(description="Search pattern (regex or text)")
|
|
29
|
-
file_types: List[str] = Field(
|
|
30
|
-
default=["*.py", "*.ipynb"],
|
|
31
|
-
description="File patterns to search (e.g., ['*.py', '*.ipynb'])",
|
|
32
|
-
)
|
|
33
|
-
path: str = Field(default=".", description="Directory to search in")
|
|
34
|
-
max_results: int = Field(default=50, description="Maximum number of results")
|
|
35
|
-
case_sensitive: bool = Field(default=False, description="Case-sensitive search")
|
|
36
|
-
execution_result: Optional[Dict[str, Any]] = Field(
|
|
37
|
-
default=None,
|
|
38
|
-
description="Execution result payload from the client",
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
|
|
42
22
|
class SearchNotebookCellsInput(BaseModel):
|
|
43
23
|
"""Input schema for search_notebook_cells tool"""
|
|
44
24
|
|
|
@@ -58,72 +38,6 @@ class SearchNotebookCellsInput(BaseModel):
|
|
|
58
38
|
)
|
|
59
39
|
|
|
60
40
|
|
|
61
|
-
def _is_ripgrep_available() -> bool:
|
|
62
|
-
"""Check if ripgrep (rg) is installed and available."""
|
|
63
|
-
return shutil.which("rg") is not None
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _build_grep_command(
|
|
67
|
-
pattern: str,
|
|
68
|
-
file_types: List[str],
|
|
69
|
-
path: str,
|
|
70
|
-
case_sensitive: bool,
|
|
71
|
-
max_results: int,
|
|
72
|
-
) -> tuple[str, str]:
|
|
73
|
-
"""
|
|
74
|
-
Build a grep/ripgrep command for searching files.
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
Tuple of (command_string, tool_name) where tool_name is 'rg' or 'grep'
|
|
78
|
-
"""
|
|
79
|
-
# Check ripgrep availability (this check will also be done on client)
|
|
80
|
-
use_ripgrep = _is_ripgrep_available()
|
|
81
|
-
|
|
82
|
-
if use_ripgrep:
|
|
83
|
-
# Build ripgrep command
|
|
84
|
-
cmd_parts = ["rg", "--line-number", "--with-filename"]
|
|
85
|
-
|
|
86
|
-
if not case_sensitive:
|
|
87
|
-
cmd_parts.append("--ignore-case")
|
|
88
|
-
|
|
89
|
-
# Add file type filters using glob patterns
|
|
90
|
-
for ft in file_types:
|
|
91
|
-
cmd_parts.extend(["--glob", ft])
|
|
92
|
-
|
|
93
|
-
# Limit results
|
|
94
|
-
cmd_parts.extend(["--max-count", str(max_results)])
|
|
95
|
-
|
|
96
|
-
# Escape pattern for shell
|
|
97
|
-
escaped_pattern = pattern.replace("'", "'\\''")
|
|
98
|
-
cmd_parts.append(f"'{escaped_pattern}'")
|
|
99
|
-
cmd_parts.append(path)
|
|
100
|
-
|
|
101
|
-
return " ".join(cmd_parts), "rg"
|
|
102
|
-
else:
|
|
103
|
-
# Build find + grep command for cross-platform compatibility
|
|
104
|
-
find_parts = ["find", path, "-type", "f", "("]
|
|
105
|
-
|
|
106
|
-
for i, ft in enumerate(file_types):
|
|
107
|
-
if i > 0:
|
|
108
|
-
find_parts.append("-o")
|
|
109
|
-
find_parts.extend(["-name", f"'{ft}'"])
|
|
110
|
-
|
|
111
|
-
find_parts.append(")")
|
|
112
|
-
|
|
113
|
-
# Add grep with proper flags
|
|
114
|
-
grep_flags = "-n" # Line numbers
|
|
115
|
-
if not case_sensitive:
|
|
116
|
-
grep_flags += "i"
|
|
117
|
-
|
|
118
|
-
# Escape pattern for shell
|
|
119
|
-
escaped_pattern = pattern.replace("'", "'\\''")
|
|
120
|
-
|
|
121
|
-
# Combine with xargs for efficiency
|
|
122
|
-
cmd = f"{' '.join(find_parts)} 2>/dev/null | xargs grep -{grep_flags} '{escaped_pattern}' 2>/dev/null | head -n {max_results}"
|
|
123
|
-
|
|
124
|
-
return cmd, "grep"
|
|
125
|
-
|
|
126
|
-
|
|
127
41
|
def _build_notebook_search_command(
|
|
128
42
|
pattern: str,
|
|
129
43
|
notebook_path: Optional[str],
|
|
@@ -139,74 +53,6 @@ def _build_notebook_search_command(
|
|
|
139
53
|
)
|
|
140
54
|
|
|
141
55
|
|
|
142
|
-
@tool(args_schema=SearchWorkspaceInput)
|
|
143
|
-
def search_workspace_tool(
|
|
144
|
-
pattern: str,
|
|
145
|
-
file_types: List[str] = None,
|
|
146
|
-
path: str = ".",
|
|
147
|
-
max_results: int = 50,
|
|
148
|
-
case_sensitive: bool = False,
|
|
149
|
-
execution_result: Optional[Dict[str, Any]] = None,
|
|
150
|
-
workspace_root: str = ".",
|
|
151
|
-
) -> Dict[str, Any]:
|
|
152
|
-
"""
|
|
153
|
-
Search for a pattern across files in the workspace.
|
|
154
|
-
|
|
155
|
-
This tool is executed on the client side using subprocess (grep/ripgrep).
|
|
156
|
-
Searches both regular files and Jupyter notebooks.
|
|
157
|
-
|
|
158
|
-
Args:
|
|
159
|
-
pattern: Search pattern (regex or text)
|
|
160
|
-
file_types: File patterns to search (default: ['*.py', '*.ipynb'])
|
|
161
|
-
path: Directory to search in (relative to workspace)
|
|
162
|
-
max_results: Maximum number of results to return
|
|
163
|
-
case_sensitive: Whether search is case-sensitive
|
|
164
|
-
|
|
165
|
-
Returns:
|
|
166
|
-
Dict with search results or pending_execution status
|
|
167
|
-
"""
|
|
168
|
-
if file_types is None:
|
|
169
|
-
file_types = ["*.py", "*.ipynb"]
|
|
170
|
-
|
|
171
|
-
# Build the search command
|
|
172
|
-
command, tool_used = _build_grep_command(
|
|
173
|
-
pattern=pattern,
|
|
174
|
-
file_types=file_types,
|
|
175
|
-
path=path,
|
|
176
|
-
case_sensitive=case_sensitive,
|
|
177
|
-
max_results=max_results,
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
response: Dict[str, Any] = {
|
|
181
|
-
"tool": "search_workspace_tool",
|
|
182
|
-
"parameters": {
|
|
183
|
-
"pattern": pattern,
|
|
184
|
-
"file_types": file_types,
|
|
185
|
-
"path": path,
|
|
186
|
-
"max_results": max_results,
|
|
187
|
-
"case_sensitive": case_sensitive,
|
|
188
|
-
},
|
|
189
|
-
"command": command,
|
|
190
|
-
"tool_used": tool_used,
|
|
191
|
-
"status": "pending_execution",
|
|
192
|
-
"message": "Search queued for execution by client",
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if execution_result is not None:
|
|
196
|
-
response["execution_result"] = execution_result
|
|
197
|
-
response["status"] = "complete"
|
|
198
|
-
response["message"] = "Search executed with client-reported results"
|
|
199
|
-
# Parse the execution result to extract search results
|
|
200
|
-
if isinstance(execution_result, dict):
|
|
201
|
-
response["success"] = execution_result.get("success", False)
|
|
202
|
-
response["results"] = execution_result.get("results", [])
|
|
203
|
-
response["total_results"] = execution_result.get("total_results", 0)
|
|
204
|
-
if "error" in execution_result:
|
|
205
|
-
response["error"] = execution_result["error"]
|
|
206
|
-
|
|
207
|
-
return response
|
|
208
|
-
|
|
209
|
-
|
|
210
56
|
@tool(args_schema=SearchNotebookCellsInput)
|
|
211
57
|
def search_notebook_cells_tool(
|
|
212
58
|
pattern: str,
|
|
@@ -281,11 +127,10 @@ def create_search_tools(workspace_root: str = ".") -> List:
|
|
|
281
127
|
Note: workspace_root is not used since tools return pending_execution
|
|
282
128
|
and actual execution happens on the client side.
|
|
283
129
|
"""
|
|
284
|
-
return [
|
|
130
|
+
return [search_notebook_cells_tool]
|
|
285
131
|
|
|
286
132
|
|
|
287
133
|
# Export all tools
|
|
288
134
|
SEARCH_TOOLS = [
|
|
289
|
-
search_workspace_tool,
|
|
290
135
|
search_notebook_cells_tool,
|
|
291
136
|
]
|
agent_server/main.py
CHANGED
|
@@ -32,6 +32,13 @@ logging.basicConfig(
|
|
|
32
32
|
)
|
|
33
33
|
logger = logging.getLogger(__name__)
|
|
34
34
|
|
|
35
|
+
# Reduce verbose logging from LangChain/LangGraph
|
|
36
|
+
# These libraries log entire message histories which creates excessive duplicate logs
|
|
37
|
+
logging.getLogger("langchain").setLevel(logging.WARNING)
|
|
38
|
+
logging.getLogger("langchain_core").setLevel(logging.WARNING)
|
|
39
|
+
logging.getLogger("langgraph").setLevel(logging.WARNING)
|
|
40
|
+
logging.getLogger("langsmith").setLevel(logging.WARNING)
|
|
41
|
+
|
|
35
42
|
|
|
36
43
|
@asynccontextmanager
|
|
37
44
|
async def lifespan(app: FastAPI):
|
|
@@ -561,23 +561,50 @@ async def stream_agent(request: AgentRequest):
|
|
|
561
561
|
# Prepare config with thread_id
|
|
562
562
|
config = {"configurable": {"thread_id": thread_id}}
|
|
563
563
|
|
|
564
|
-
#
|
|
564
|
+
# Check existing state and reset todos if all completed
|
|
565
|
+
should_reset_todos = False
|
|
565
566
|
try:
|
|
566
567
|
existing_state = checkpointer.get(config)
|
|
567
568
|
if existing_state:
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
)
|
|
569
|
+
channel_values = existing_state.get("channel_values", {})
|
|
570
|
+
existing_messages = channel_values.get("messages", [])
|
|
571
|
+
existing_todos = channel_values.get("todos", [])
|
|
571
572
|
logger.info(
|
|
572
|
-
"Existing state for thread %s: %d messages found",
|
|
573
|
+
"Existing state for thread %s: %d messages, %d todos found",
|
|
573
574
|
thread_id,
|
|
574
575
|
len(existing_messages),
|
|
576
|
+
len(existing_todos),
|
|
575
577
|
)
|
|
578
|
+
# Check if all todos are completed - if so, reset them
|
|
579
|
+
if existing_todos:
|
|
580
|
+
all_completed = all(
|
|
581
|
+
t.get("status") == "completed" for t in existing_todos
|
|
582
|
+
)
|
|
583
|
+
if all_completed:
|
|
584
|
+
should_reset_todos = True
|
|
585
|
+
logger.info(
|
|
586
|
+
"All %d todos are completed, will reset for new task",
|
|
587
|
+
len(existing_todos),
|
|
588
|
+
)
|
|
576
589
|
else:
|
|
577
590
|
logger.info("No existing state for thread %s", thread_id)
|
|
578
591
|
except Exception as e:
|
|
579
592
|
logger.warning("Could not check existing state: %s", e)
|
|
580
593
|
|
|
594
|
+
# Reset todos in agent state if all were completed
|
|
595
|
+
todos_reset_event = None
|
|
596
|
+
if should_reset_todos:
|
|
597
|
+
try:
|
|
598
|
+
agent.update_state(config, {"todos": []})
|
|
599
|
+
logger.info("Reset todos in agent state for thread %s", thread_id)
|
|
600
|
+
# Prepare event to notify frontend (will be yielded after function setup)
|
|
601
|
+
todos_reset_event = {
|
|
602
|
+
"event": "todos",
|
|
603
|
+
"data": json.dumps({"todos": [], "reset": True}),
|
|
604
|
+
}
|
|
605
|
+
except Exception as e:
|
|
606
|
+
logger.warning("Could not reset todos in agent state: %s", e)
|
|
607
|
+
|
|
581
608
|
# Prepare input
|
|
582
609
|
agent_input = {"messages": [{"role": "user", "content": request.request}]}
|
|
583
610
|
|
|
@@ -593,6 +620,11 @@ async def stream_agent(request: AgentRequest):
|
|
|
593
620
|
emitted_contents: set = set()
|
|
594
621
|
_simple_agent_emitted_contents[thread_id] = emitted_contents
|
|
595
622
|
|
|
623
|
+
# Emit todos reset event if needed (before starting the stream)
|
|
624
|
+
if todos_reset_event:
|
|
625
|
+
logger.info("SSE: Emitting todos reset event")
|
|
626
|
+
yield todos_reset_event
|
|
627
|
+
|
|
596
628
|
# Initial status: waiting for LLM
|
|
597
629
|
logger.info("SSE: Sending initial debug status '🤔 LLM 응답 대기 중'")
|
|
598
630
|
yield {
|
|
@@ -670,6 +702,27 @@ async def stream_agent(request: AgentRequest):
|
|
|
670
702
|
"event": "todos",
|
|
671
703
|
"data": json.dumps({"todos": todos}),
|
|
672
704
|
}
|
|
705
|
+
# Check if all todos are completed - auto terminate
|
|
706
|
+
all_completed = all(
|
|
707
|
+
t.get("status") == "completed" for t in todos
|
|
708
|
+
)
|
|
709
|
+
if all_completed and len(todos) > 0:
|
|
710
|
+
logger.info(
|
|
711
|
+
"All %d todos completed, auto-terminating agent",
|
|
712
|
+
len(todos),
|
|
713
|
+
)
|
|
714
|
+
yield {
|
|
715
|
+
"event": "debug_clear",
|
|
716
|
+
"data": json.dumps({}),
|
|
717
|
+
}
|
|
718
|
+
yield {
|
|
719
|
+
"event": "done",
|
|
720
|
+
"data": json.dumps(
|
|
721
|
+
{"reason": "all_todos_completed"}
|
|
722
|
+
),
|
|
723
|
+
}
|
|
724
|
+
return # Exit the generator
|
|
725
|
+
|
|
673
726
|
tool_name = getattr(last_message, "name", "") or ""
|
|
674
727
|
logger.info(
|
|
675
728
|
"SimpleAgent ToolMessage name attribute: %s", tool_name
|
|
@@ -695,17 +748,49 @@ async def stream_agent(request: AgentRequest):
|
|
|
695
748
|
final_answer = tool_result.get(
|
|
696
749
|
"answer"
|
|
697
750
|
) or tool_result.get("parameters", {}).get("answer")
|
|
751
|
+
|
|
752
|
+
# Check for next_items in answer field (LLM may put JSON here)
|
|
753
|
+
if final_answer:
|
|
754
|
+
try:
|
|
755
|
+
answer_json = json.loads(final_answer)
|
|
756
|
+
if "next_items" in answer_json:
|
|
757
|
+
next_items_block = f"\n\n```json\n{json.dumps(answer_json, ensure_ascii=False, indent=2)}\n```"
|
|
758
|
+
# Get summary for the main text
|
|
759
|
+
summary_text = (
|
|
760
|
+
tool_result.get("summary")
|
|
761
|
+
or tool_result.get(
|
|
762
|
+
"parameters", {}
|
|
763
|
+
).get("summary")
|
|
764
|
+
or ""
|
|
765
|
+
)
|
|
766
|
+
final_answer = (
|
|
767
|
+
summary_text + next_items_block
|
|
768
|
+
)
|
|
769
|
+
logger.info(
|
|
770
|
+
"Extracted next_items from answer field"
|
|
771
|
+
)
|
|
772
|
+
except (json.JSONDecodeError, TypeError):
|
|
773
|
+
pass
|
|
774
|
+
|
|
698
775
|
# Check for next_items in summary field (Gemini puts JSON here)
|
|
699
776
|
summary = tool_result.get(
|
|
700
777
|
"summary"
|
|
701
|
-
) or tool_result.get("parameters", {}).get(
|
|
702
|
-
|
|
778
|
+
) or tool_result.get("parameters", {}).get(
|
|
779
|
+
"summary"
|
|
780
|
+
)
|
|
781
|
+
if summary and "next_items" not in (
|
|
782
|
+
final_answer or ""
|
|
783
|
+
):
|
|
703
784
|
try:
|
|
704
785
|
summary_json = json.loads(summary)
|
|
705
786
|
if "next_items" in summary_json:
|
|
706
787
|
next_items_block = f"\n\n```json\n{json.dumps(summary_json, ensure_ascii=False, indent=2)}\n```"
|
|
707
|
-
final_answer = (
|
|
708
|
-
|
|
788
|
+
final_answer = (
|
|
789
|
+
final_answer or ""
|
|
790
|
+
) + next_items_block
|
|
791
|
+
logger.info(
|
|
792
|
+
"Extracted next_items from summary field"
|
|
793
|
+
)
|
|
709
794
|
except (json.JSONDecodeError, TypeError):
|
|
710
795
|
pass
|
|
711
796
|
if final_answer:
|
|
@@ -830,13 +915,6 @@ async def stream_agent(request: AgentRequest):
|
|
|
830
915
|
|
|
831
916
|
# Create detailed status message for search tools
|
|
832
917
|
if tool_name in (
|
|
833
|
-
"search_workspace_tool",
|
|
834
|
-
"search_workspace",
|
|
835
|
-
):
|
|
836
|
-
pattern = tool_args.get("pattern", "")
|
|
837
|
-
path = tool_args.get("path", ".")
|
|
838
|
-
status_msg = f"🔍 검색 실행: grep/rg '{pattern}' in {path}"
|
|
839
|
-
elif tool_name in (
|
|
840
918
|
"search_notebook_cells_tool",
|
|
841
919
|
"search_notebook_cells",
|
|
842
920
|
):
|
|
@@ -902,34 +980,6 @@ async def stream_agent(request: AgentRequest):
|
|
|
902
980
|
}
|
|
903
981
|
),
|
|
904
982
|
}
|
|
905
|
-
elif tool_name in (
|
|
906
|
-
"search_workspace_tool",
|
|
907
|
-
"search_workspace",
|
|
908
|
-
):
|
|
909
|
-
# Search workspace - emit tool_call for client-side execution
|
|
910
|
-
produced_output = True
|
|
911
|
-
yield {
|
|
912
|
-
"event": "tool_call",
|
|
913
|
-
"data": json.dumps(
|
|
914
|
-
{
|
|
915
|
-
"tool": "search_workspace",
|
|
916
|
-
"pattern": tool_args.get(
|
|
917
|
-
"pattern", ""
|
|
918
|
-
),
|
|
919
|
-
"file_types": tool_args.get(
|
|
920
|
-
"file_types",
|
|
921
|
-
["*.py", "*.ipynb"],
|
|
922
|
-
),
|
|
923
|
-
"path": tool_args.get("path", "."),
|
|
924
|
-
"max_results": tool_args.get(
|
|
925
|
-
"max_results", 50
|
|
926
|
-
),
|
|
927
|
-
"case_sensitive": tool_args.get(
|
|
928
|
-
"case_sensitive", False
|
|
929
|
-
),
|
|
930
|
-
}
|
|
931
|
-
),
|
|
932
|
-
}
|
|
933
983
|
elif tool_name in (
|
|
934
984
|
"search_notebook_cells_tool",
|
|
935
985
|
"search_notebook_cells",
|
|
@@ -1113,7 +1163,7 @@ async def stream_agent(request: AgentRequest):
|
|
|
1113
1163
|
content=(
|
|
1114
1164
|
"You MUST respond with a valid tool call. "
|
|
1115
1165
|
"Available tools: jupyter_cell_tool (for Python code), markdown_tool (for text), "
|
|
1116
|
-
"
|
|
1166
|
+
"execute_command_tool (to search files with find/grep), read_file_tool (to read files). "
|
|
1117
1167
|
"Choose the most appropriate tool and provide valid JSON arguments."
|
|
1118
1168
|
)
|
|
1119
1169
|
),
|
|
@@ -1232,11 +1282,7 @@ async def stream_agent(request: AgentRequest):
|
|
|
1232
1282
|
}
|
|
1233
1283
|
),
|
|
1234
1284
|
}
|
|
1235
|
-
elif tool_name
|
|
1236
|
-
"read_file_tool",
|
|
1237
|
-
"list_files_tool",
|
|
1238
|
-
"search_workspace_tool",
|
|
1239
|
-
):
|
|
1285
|
+
elif tool_name == "read_file_tool":
|
|
1240
1286
|
# For file operations, generate code with the LLM
|
|
1241
1287
|
logger.info(
|
|
1242
1288
|
"Fallback: Generating code for %s via LLM",
|
|
@@ -1388,9 +1434,28 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1388
1434
|
)
|
|
1389
1435
|
# Get or create cached agent
|
|
1390
1436
|
resolved_workspace_root = _resolve_workspace_root(request.workspaceRoot)
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1437
|
+
|
|
1438
|
+
# CRITICAL: Validate checkpoint exists before resume
|
|
1439
|
+
# InMemorySaver is volatile - server restart loses all checkpoints
|
|
1440
|
+
if request.threadId not in _simple_agent_checkpointers:
|
|
1441
|
+
logger.warning(
|
|
1442
|
+
"Resume failed: No checkpoint found for thread %s. "
|
|
1443
|
+
"Server may have restarted or session expired.",
|
|
1444
|
+
request.threadId,
|
|
1445
|
+
)
|
|
1446
|
+
yield {
|
|
1447
|
+
"event": "error",
|
|
1448
|
+
"data": json.dumps(
|
|
1449
|
+
{
|
|
1450
|
+
"error": "Session expired or not found",
|
|
1451
|
+
"code": "CHECKPOINT_NOT_FOUND",
|
|
1452
|
+
"message": "이전 세션을 찾을 수 없습니다. 서버가 재시작되었거나 세션이 만료되었습니다. 새로운 대화를 시작해주세요.",
|
|
1453
|
+
}
|
|
1454
|
+
),
|
|
1455
|
+
}
|
|
1456
|
+
return
|
|
1457
|
+
|
|
1458
|
+
checkpointer = _simple_agent_checkpointers.get(request.threadId)
|
|
1394
1459
|
|
|
1395
1460
|
agent_cache_key = _get_agent_cache_key(
|
|
1396
1461
|
llm_config=config_dict,
|
|
@@ -1406,7 +1471,9 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1406
1471
|
len(_simple_agent_instances),
|
|
1407
1472
|
)
|
|
1408
1473
|
else:
|
|
1409
|
-
logger.info(
|
|
1474
|
+
logger.info(
|
|
1475
|
+
"Resume: Creating new agent for key %s", agent_cache_key[:8]
|
|
1476
|
+
)
|
|
1410
1477
|
agent = create_simple_chat_agent(
|
|
1411
1478
|
llm_config=config_dict,
|
|
1412
1479
|
workspace_root=resolved_workspace_root,
|
|
@@ -1589,6 +1656,27 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1589
1656
|
"event": "todos",
|
|
1590
1657
|
"data": json.dumps({"todos": todos}),
|
|
1591
1658
|
}
|
|
1659
|
+
# Check if all todos are completed - auto terminate
|
|
1660
|
+
all_completed = all(
|
|
1661
|
+
t.get("status") == "completed" for t in todos
|
|
1662
|
+
)
|
|
1663
|
+
if all_completed and len(todos) > 0:
|
|
1664
|
+
logger.info(
|
|
1665
|
+
"Resume: All %d todos completed, auto-terminating agent",
|
|
1666
|
+
len(todos),
|
|
1667
|
+
)
|
|
1668
|
+
yield {
|
|
1669
|
+
"event": "debug_clear",
|
|
1670
|
+
"data": json.dumps({}),
|
|
1671
|
+
}
|
|
1672
|
+
yield {
|
|
1673
|
+
"event": "done",
|
|
1674
|
+
"data": json.dumps(
|
|
1675
|
+
{"reason": "all_todos_completed"}
|
|
1676
|
+
),
|
|
1677
|
+
}
|
|
1678
|
+
return # Exit the generator
|
|
1679
|
+
|
|
1592
1680
|
tool_name = getattr(last_message, "name", "") or ""
|
|
1593
1681
|
logger.info(
|
|
1594
1682
|
"Resume ToolMessage name attribute: %s", tool_name
|
|
@@ -1612,17 +1700,49 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1612
1700
|
final_answer = tool_result.get(
|
|
1613
1701
|
"answer"
|
|
1614
1702
|
) or tool_result.get("parameters", {}).get("answer")
|
|
1703
|
+
|
|
1704
|
+
# Check for next_items in answer field (LLM may put JSON here)
|
|
1705
|
+
if final_answer:
|
|
1706
|
+
try:
|
|
1707
|
+
answer_json = json.loads(final_answer)
|
|
1708
|
+
if "next_items" in answer_json:
|
|
1709
|
+
next_items_block = f"\n\n```json\n{json.dumps(answer_json, ensure_ascii=False, indent=2)}\n```"
|
|
1710
|
+
# Get summary for the main text
|
|
1711
|
+
summary_text = (
|
|
1712
|
+
tool_result.get("summary")
|
|
1713
|
+
or tool_result.get(
|
|
1714
|
+
"parameters", {}
|
|
1715
|
+
).get("summary")
|
|
1716
|
+
or ""
|
|
1717
|
+
)
|
|
1718
|
+
final_answer = (
|
|
1719
|
+
summary_text + next_items_block
|
|
1720
|
+
)
|
|
1721
|
+
logger.info(
|
|
1722
|
+
"Resume: Extracted next_items from answer field"
|
|
1723
|
+
)
|
|
1724
|
+
except (json.JSONDecodeError, TypeError):
|
|
1725
|
+
pass
|
|
1726
|
+
|
|
1615
1727
|
# Check for next_items in summary field (Gemini puts JSON here)
|
|
1616
1728
|
summary = tool_result.get(
|
|
1617
1729
|
"summary"
|
|
1618
|
-
) or tool_result.get("parameters", {}).get(
|
|
1619
|
-
|
|
1730
|
+
) or tool_result.get("parameters", {}).get(
|
|
1731
|
+
"summary"
|
|
1732
|
+
)
|
|
1733
|
+
if summary and "next_items" not in (
|
|
1734
|
+
final_answer or ""
|
|
1735
|
+
):
|
|
1620
1736
|
try:
|
|
1621
1737
|
summary_json = json.loads(summary)
|
|
1622
1738
|
if "next_items" in summary_json:
|
|
1623
1739
|
next_items_block = f"\n\n```json\n{json.dumps(summary_json, ensure_ascii=False, indent=2)}\n```"
|
|
1624
|
-
final_answer = (
|
|
1625
|
-
|
|
1740
|
+
final_answer = (
|
|
1741
|
+
final_answer or ""
|
|
1742
|
+
) + next_items_block
|
|
1743
|
+
logger.info(
|
|
1744
|
+
"Resume: Extracted next_items from summary field"
|
|
1745
|
+
)
|
|
1626
1746
|
except (json.JSONDecodeError, TypeError):
|
|
1627
1747
|
pass
|
|
1628
1748
|
if final_answer:
|
|
@@ -1769,13 +1889,6 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1769
1889
|
|
|
1770
1890
|
# Create detailed status message for search tools
|
|
1771
1891
|
if tool_name in (
|
|
1772
|
-
"search_workspace_tool",
|
|
1773
|
-
"search_workspace",
|
|
1774
|
-
):
|
|
1775
|
-
pattern = tool_args.get("pattern", "")
|
|
1776
|
-
path = tool_args.get("path", ".")
|
|
1777
|
-
status_msg = f"🔍 검색 실행: grep/rg '{pattern}' in {path}"
|
|
1778
|
-
elif tool_name in (
|
|
1779
1892
|
"search_notebook_cells_tool",
|
|
1780
1893
|
"search_notebook_cells",
|
|
1781
1894
|
):
|
|
@@ -1833,33 +1946,6 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1833
1946
|
}
|
|
1834
1947
|
),
|
|
1835
1948
|
}
|
|
1836
|
-
elif tool_name in (
|
|
1837
|
-
"search_workspace_tool",
|
|
1838
|
-
"search_workspace",
|
|
1839
|
-
):
|
|
1840
|
-
# Search workspace - emit tool_call for client-side execution
|
|
1841
|
-
yield {
|
|
1842
|
-
"event": "tool_call",
|
|
1843
|
-
"data": json.dumps(
|
|
1844
|
-
{
|
|
1845
|
-
"tool": "search_workspace",
|
|
1846
|
-
"pattern": tool_args.get(
|
|
1847
|
-
"pattern", ""
|
|
1848
|
-
),
|
|
1849
|
-
"file_types": tool_args.get(
|
|
1850
|
-
"file_types",
|
|
1851
|
-
["*.py", "*.ipynb"],
|
|
1852
|
-
),
|
|
1853
|
-
"path": tool_args.get("path", "."),
|
|
1854
|
-
"max_results": tool_args.get(
|
|
1855
|
-
"max_results", 50
|
|
1856
|
-
),
|
|
1857
|
-
"case_sensitive": tool_args.get(
|
|
1858
|
-
"case_sensitive", False
|
|
1859
|
-
),
|
|
1860
|
-
}
|
|
1861
|
-
),
|
|
1862
|
-
}
|
|
1863
1949
|
elif tool_name in (
|
|
1864
1950
|
"search_notebook_cells_tool",
|
|
1865
1951
|
"search_notebook_cells",
|
|
@@ -1966,16 +2052,35 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1966
2052
|
}
|
|
1967
2053
|
|
|
1968
2054
|
except Exception as e:
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2055
|
+
error_msg = str(e)
|
|
2056
|
+
logger.error(f"Resume error: {error_msg}", exc_info=True)
|
|
2057
|
+
|
|
2058
|
+
# Detect specific Gemini error for empty contents
|
|
2059
|
+
if "contents is not specified" in error_msg.lower():
|
|
2060
|
+
logger.warning(
|
|
2061
|
+
"Detected 'contents is not specified' error - likely session state loss"
|
|
2062
|
+
)
|
|
2063
|
+
yield {
|
|
2064
|
+
"event": "error",
|
|
2065
|
+
"data": json.dumps(
|
|
2066
|
+
{
|
|
2067
|
+
"error": "Session state lost",
|
|
2068
|
+
"code": "CONTENTS_NOT_SPECIFIED",
|
|
2069
|
+
"error_type": type(e).__name__,
|
|
2070
|
+
"message": "세션 상태가 손실되었습니다. 서버가 재시작되었거나 세션이 만료되었습니다. 새로운 대화를 시작해주세요.",
|
|
2071
|
+
}
|
|
2072
|
+
),
|
|
2073
|
+
}
|
|
2074
|
+
else:
|
|
2075
|
+
yield {
|
|
2076
|
+
"event": "error",
|
|
2077
|
+
"data": json.dumps(
|
|
2078
|
+
{
|
|
2079
|
+
"error": error_msg,
|
|
2080
|
+
"error_type": type(e).__name__,
|
|
2081
|
+
}
|
|
2082
|
+
),
|
|
2083
|
+
}
|
|
1979
2084
|
|
|
1980
2085
|
return EventSourceResponse(event_generator())
|
|
1981
2086
|
|