hdsp-jupyter-extension 2.0.1__py3-none-any.whl → 2.0.3__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.3.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.1.data → hdsp_jupyter_extension-2.0.3.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.3.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.634cf0ae0f3592d0882f.js +408 -4
- hdsp_jupyter_extension-2.0.3.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.3.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.1366019c413f1d68467f.js +753 -65
- hdsp_jupyter_extension-2.0.3.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.3.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.b6d91b150c0800bddfa4.js +8 -8
- hdsp_jupyter_extension-2.0.3.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.b6d91b150c0800bddfa4.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.3.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.3.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.3.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.3.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.3.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.3.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.3.dist-info}/METADATA +6 -1
- {hdsp_jupyter_extension-2.0.1.dist-info → hdsp_jupyter_extension-2.0.3.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.b6d91b150c0800bddfa4.js} +8 -8
- jupyter_ext/labextension/static/remoteEntry.b6d91b150c0800bddfa4.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.3.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.3.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.1.data → hdsp_jupyter_extension-2.0.3.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.3.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.3.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.3.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.3.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.1.data → hdsp_jupyter_extension-2.0.3.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.3.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.3.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.3.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.3.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.3.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.3.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.1.dist-info → hdsp_jupyter_extension-2.0.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Notebook Searcher
|
|
3
|
+
|
|
4
|
+
Provides search functionality for Jupyter notebooks and workspace files.
|
|
5
|
+
Supports searching:
|
|
6
|
+
- Across all files in workspace
|
|
7
|
+
- Within specific notebooks
|
|
8
|
+
- By cell type (code/markdown)
|
|
9
|
+
- Using regex or text patterns
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class SearchMatch:
|
|
24
|
+
"""Single search match result"""
|
|
25
|
+
file_path: str
|
|
26
|
+
cell_index: Optional[int] = None
|
|
27
|
+
cell_type: Optional[str] = None
|
|
28
|
+
line_number: Optional[int] = None
|
|
29
|
+
content: str = ""
|
|
30
|
+
context_before: str = ""
|
|
31
|
+
context_after: str = ""
|
|
32
|
+
match_type: str = "text" # "text", "cell", "line"
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
35
|
+
return {
|
|
36
|
+
"file_path": self.file_path,
|
|
37
|
+
"cell_index": self.cell_index,
|
|
38
|
+
"cell_type": self.cell_type,
|
|
39
|
+
"line_number": self.line_number,
|
|
40
|
+
"content": self.content,
|
|
41
|
+
"context_before": self.context_before,
|
|
42
|
+
"context_after": self.context_after,
|
|
43
|
+
"match_type": self.match_type,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class SearchResults:
|
|
49
|
+
"""Collection of search results"""
|
|
50
|
+
query: str
|
|
51
|
+
total_matches: int
|
|
52
|
+
files_searched: int
|
|
53
|
+
matches: List[SearchMatch] = field(default_factory=list)
|
|
54
|
+
truncated: bool = False
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
57
|
+
return {
|
|
58
|
+
"query": self.query,
|
|
59
|
+
"total_matches": self.total_matches,
|
|
60
|
+
"files_searched": self.files_searched,
|
|
61
|
+
"matches": [m.to_dict() for m in self.matches],
|
|
62
|
+
"truncated": self.truncated,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class NotebookSearcher:
|
|
67
|
+
"""
|
|
68
|
+
Searches notebooks and workspace files for patterns.
|
|
69
|
+
|
|
70
|
+
Features:
|
|
71
|
+
- Search across all files in workspace
|
|
72
|
+
- Search within specific notebooks
|
|
73
|
+
- Filter by cell type (code/markdown)
|
|
74
|
+
- Regex or literal text matching
|
|
75
|
+
- Context lines around matches
|
|
76
|
+
|
|
77
|
+
Usage:
|
|
78
|
+
searcher = NotebookSearcher(workspace_root="/path/to/workspace")
|
|
79
|
+
results = searcher.search_workspace("import pandas")
|
|
80
|
+
results = searcher.search_notebook("analysis.ipynb", "df.head()")
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, workspace_root: str = "."):
|
|
84
|
+
self.workspace_root = os.path.abspath(workspace_root)
|
|
85
|
+
self._contents_manager = None
|
|
86
|
+
|
|
87
|
+
def set_contents_manager(self, contents_manager: Any):
|
|
88
|
+
"""Set Jupyter contents manager for direct notebook access"""
|
|
89
|
+
self._contents_manager = contents_manager
|
|
90
|
+
|
|
91
|
+
def _compile_pattern(
|
|
92
|
+
self,
|
|
93
|
+
pattern: str,
|
|
94
|
+
case_sensitive: bool = False,
|
|
95
|
+
is_regex: bool = False,
|
|
96
|
+
) -> re.Pattern:
|
|
97
|
+
"""Compile search pattern"""
|
|
98
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
99
|
+
|
|
100
|
+
if not is_regex:
|
|
101
|
+
pattern = re.escape(pattern)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
return re.compile(pattern, flags)
|
|
105
|
+
except re.error as e:
|
|
106
|
+
logger.warning(f"Invalid regex pattern: {e}, using literal")
|
|
107
|
+
return re.compile(re.escape(pattern), flags)
|
|
108
|
+
|
|
109
|
+
def _read_notebook(self, path: str) -> Optional[Dict[str, Any]]:
|
|
110
|
+
"""Read a notebook file"""
|
|
111
|
+
full_path = os.path.join(self.workspace_root, path)
|
|
112
|
+
|
|
113
|
+
# Try contents manager first
|
|
114
|
+
if self._contents_manager:
|
|
115
|
+
try:
|
|
116
|
+
model = self._contents_manager.get(path, content=True)
|
|
117
|
+
return model.get("content")
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
# Fall back to file read
|
|
122
|
+
try:
|
|
123
|
+
with open(full_path, "r", encoding="utf-8") as f:
|
|
124
|
+
return json.load(f)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.error(f"Failed to read notebook {path}: {e}")
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
def _get_context(
|
|
130
|
+
self,
|
|
131
|
+
lines: List[str],
|
|
132
|
+
line_idx: int,
|
|
133
|
+
context_lines: int = 2,
|
|
134
|
+
) -> tuple:
|
|
135
|
+
"""Get context lines before and after a match"""
|
|
136
|
+
start = max(0, line_idx - context_lines)
|
|
137
|
+
end = min(len(lines), line_idx + context_lines + 1)
|
|
138
|
+
|
|
139
|
+
before = "\n".join(lines[start:line_idx])
|
|
140
|
+
after = "\n".join(lines[line_idx + 1:end])
|
|
141
|
+
|
|
142
|
+
return before, after
|
|
143
|
+
|
|
144
|
+
def search_notebook(
|
|
145
|
+
self,
|
|
146
|
+
notebook_path: str,
|
|
147
|
+
pattern: str,
|
|
148
|
+
cell_type: Optional[str] = None,
|
|
149
|
+
case_sensitive: bool = False,
|
|
150
|
+
is_regex: bool = False,
|
|
151
|
+
max_results: int = 50,
|
|
152
|
+
context_lines: int = 2,
|
|
153
|
+
) -> SearchResults:
|
|
154
|
+
"""
|
|
155
|
+
Search within a specific notebook.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
notebook_path: Path to notebook (relative to workspace)
|
|
159
|
+
pattern: Search pattern
|
|
160
|
+
cell_type: Filter by cell type ("code" or "markdown")
|
|
161
|
+
case_sensitive: Case-sensitive search
|
|
162
|
+
is_regex: Treat pattern as regex
|
|
163
|
+
max_results: Maximum matches to return
|
|
164
|
+
context_lines: Context lines around matches
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
SearchResults with matches
|
|
168
|
+
"""
|
|
169
|
+
compiled = self._compile_pattern(pattern, case_sensitive, is_regex)
|
|
170
|
+
matches: List[SearchMatch] = []
|
|
171
|
+
|
|
172
|
+
notebook = self._read_notebook(notebook_path)
|
|
173
|
+
if not notebook:
|
|
174
|
+
return SearchResults(
|
|
175
|
+
query=pattern,
|
|
176
|
+
total_matches=0,
|
|
177
|
+
files_searched=1,
|
|
178
|
+
matches=[],
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
cells = notebook.get("cells", [])
|
|
182
|
+
|
|
183
|
+
for idx, cell in enumerate(cells):
|
|
184
|
+
current_type = cell.get("cell_type", "code")
|
|
185
|
+
|
|
186
|
+
# Filter by cell type
|
|
187
|
+
if cell_type and current_type != cell_type:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
source = cell.get("source", "")
|
|
191
|
+
if isinstance(source, list):
|
|
192
|
+
source = "".join(source)
|
|
193
|
+
|
|
194
|
+
if not compiled.search(source):
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# Find specific matching lines
|
|
198
|
+
lines = source.split("\n")
|
|
199
|
+
for line_idx, line in enumerate(lines):
|
|
200
|
+
if compiled.search(line):
|
|
201
|
+
before, after = self._get_context(lines, line_idx, context_lines)
|
|
202
|
+
|
|
203
|
+
matches.append(SearchMatch(
|
|
204
|
+
file_path=notebook_path,
|
|
205
|
+
cell_index=idx,
|
|
206
|
+
cell_type=current_type,
|
|
207
|
+
line_number=line_idx + 1,
|
|
208
|
+
content=line.strip()[:200],
|
|
209
|
+
context_before=before[:100],
|
|
210
|
+
context_after=after[:100],
|
|
211
|
+
match_type="line",
|
|
212
|
+
))
|
|
213
|
+
|
|
214
|
+
if len(matches) >= max_results:
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
if len(matches) >= max_results:
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
return SearchResults(
|
|
221
|
+
query=pattern,
|
|
222
|
+
total_matches=len(matches),
|
|
223
|
+
files_searched=1,
|
|
224
|
+
matches=matches,
|
|
225
|
+
truncated=len(matches) >= max_results,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def search_workspace(
|
|
229
|
+
self,
|
|
230
|
+
pattern: str,
|
|
231
|
+
file_patterns: Optional[List[str]] = None,
|
|
232
|
+
path: str = ".",
|
|
233
|
+
case_sensitive: bool = False,
|
|
234
|
+
is_regex: bool = False,
|
|
235
|
+
max_results: int = 100,
|
|
236
|
+
include_notebooks: bool = True,
|
|
237
|
+
include_python: bool = True,
|
|
238
|
+
) -> SearchResults:
|
|
239
|
+
"""
|
|
240
|
+
Search across workspace files.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
pattern: Search pattern
|
|
244
|
+
file_patterns: File glob patterns to include (e.g., ["*.py", "*.ipynb"])
|
|
245
|
+
path: Directory to search (relative to workspace)
|
|
246
|
+
case_sensitive: Case-sensitive search
|
|
247
|
+
is_regex: Treat pattern as regex
|
|
248
|
+
max_results: Maximum matches to return
|
|
249
|
+
include_notebooks: Search in .ipynb files
|
|
250
|
+
include_python: Search in .py files
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
SearchResults with matches
|
|
254
|
+
"""
|
|
255
|
+
import fnmatch
|
|
256
|
+
|
|
257
|
+
if file_patterns is None:
|
|
258
|
+
file_patterns = []
|
|
259
|
+
if include_notebooks:
|
|
260
|
+
file_patterns.append("*.ipynb")
|
|
261
|
+
if include_python:
|
|
262
|
+
file_patterns.append("*.py")
|
|
263
|
+
|
|
264
|
+
compiled = self._compile_pattern(pattern, case_sensitive, is_regex)
|
|
265
|
+
matches: List[SearchMatch] = []
|
|
266
|
+
files_searched = 0
|
|
267
|
+
|
|
268
|
+
search_path = os.path.join(self.workspace_root, path)
|
|
269
|
+
|
|
270
|
+
for root, _, filenames in os.walk(search_path):
|
|
271
|
+
for filename in filenames:
|
|
272
|
+
# Check file pattern
|
|
273
|
+
if not any(fnmatch.fnmatch(filename, p) for p in file_patterns):
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
file_path = os.path.join(root, filename)
|
|
277
|
+
rel_path = os.path.relpath(file_path, self.workspace_root)
|
|
278
|
+
files_searched += 1
|
|
279
|
+
|
|
280
|
+
if filename.endswith(".ipynb"):
|
|
281
|
+
# Search in notebook
|
|
282
|
+
nb_results = self.search_notebook(
|
|
283
|
+
rel_path,
|
|
284
|
+
pattern,
|
|
285
|
+
case_sensitive=case_sensitive,
|
|
286
|
+
is_regex=is_regex,
|
|
287
|
+
max_results=max_results - len(matches),
|
|
288
|
+
)
|
|
289
|
+
matches.extend(nb_results.matches)
|
|
290
|
+
else:
|
|
291
|
+
# Search in regular file
|
|
292
|
+
file_matches = self._search_in_file(
|
|
293
|
+
file_path,
|
|
294
|
+
rel_path,
|
|
295
|
+
compiled,
|
|
296
|
+
max_results - len(matches),
|
|
297
|
+
)
|
|
298
|
+
matches.extend(file_matches)
|
|
299
|
+
|
|
300
|
+
if len(matches) >= max_results:
|
|
301
|
+
break
|
|
302
|
+
|
|
303
|
+
if len(matches) >= max_results:
|
|
304
|
+
break
|
|
305
|
+
|
|
306
|
+
return SearchResults(
|
|
307
|
+
query=pattern,
|
|
308
|
+
total_matches=len(matches),
|
|
309
|
+
files_searched=files_searched,
|
|
310
|
+
matches=matches,
|
|
311
|
+
truncated=len(matches) >= max_results,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def _search_in_file(
|
|
315
|
+
self,
|
|
316
|
+
file_path: str,
|
|
317
|
+
rel_path: str,
|
|
318
|
+
compiled: re.Pattern,
|
|
319
|
+
max_results: int,
|
|
320
|
+
) -> List[SearchMatch]:
|
|
321
|
+
"""Search in a regular text file"""
|
|
322
|
+
matches: List[SearchMatch] = []
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
326
|
+
lines = f.readlines()
|
|
327
|
+
|
|
328
|
+
for line_idx, line in enumerate(lines):
|
|
329
|
+
if compiled.search(line):
|
|
330
|
+
before = ""
|
|
331
|
+
after = ""
|
|
332
|
+
|
|
333
|
+
if line_idx > 0:
|
|
334
|
+
before = lines[line_idx - 1].strip()[:100]
|
|
335
|
+
if line_idx < len(lines) - 1:
|
|
336
|
+
after = lines[line_idx + 1].strip()[:100]
|
|
337
|
+
|
|
338
|
+
matches.append(SearchMatch(
|
|
339
|
+
file_path=rel_path,
|
|
340
|
+
line_number=line_idx + 1,
|
|
341
|
+
content=line.strip()[:200],
|
|
342
|
+
context_before=before,
|
|
343
|
+
context_after=after,
|
|
344
|
+
match_type="line",
|
|
345
|
+
))
|
|
346
|
+
|
|
347
|
+
if len(matches) >= max_results:
|
|
348
|
+
break
|
|
349
|
+
|
|
350
|
+
except Exception as e:
|
|
351
|
+
logger.error(f"Failed to search file {file_path}: {e}")
|
|
352
|
+
|
|
353
|
+
return matches
|
|
354
|
+
|
|
355
|
+
def search_current_notebook_cells(
|
|
356
|
+
self,
|
|
357
|
+
notebook_path: str,
|
|
358
|
+
pattern: str,
|
|
359
|
+
cell_type: Optional[str] = None,
|
|
360
|
+
) -> List[Dict[str, Any]]:
|
|
361
|
+
"""
|
|
362
|
+
Search cells in the current notebook.
|
|
363
|
+
|
|
364
|
+
Convenience method for quick cell search in active notebook.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
notebook_path: Current notebook path
|
|
368
|
+
pattern: Search pattern
|
|
369
|
+
cell_type: Optional cell type filter
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
List of matching cells with their indices and content
|
|
373
|
+
"""
|
|
374
|
+
results = self.search_notebook(
|
|
375
|
+
notebook_path,
|
|
376
|
+
pattern,
|
|
377
|
+
cell_type=cell_type,
|
|
378
|
+
max_results=20,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Group by cell index
|
|
382
|
+
cells_by_index: Dict[int, Dict[str, Any]] = {}
|
|
383
|
+
|
|
384
|
+
for match in results.matches:
|
|
385
|
+
idx = match.cell_index
|
|
386
|
+
if idx not in cells_by_index:
|
|
387
|
+
cells_by_index[idx] = {
|
|
388
|
+
"cell_index": idx,
|
|
389
|
+
"cell_type": match.cell_type,
|
|
390
|
+
"matching_lines": [],
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
cells_by_index[idx]["matching_lines"].append({
|
|
394
|
+
"line_number": match.line_number,
|
|
395
|
+
"content": match.content,
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
return list(cells_by_index.values())
|
|
399
|
+
|
|
400
|
+
def get_notebook_structure(self, notebook_path: str) -> Dict[str, Any]:
|
|
401
|
+
"""
|
|
402
|
+
Get structural overview of a notebook.
|
|
403
|
+
|
|
404
|
+
Returns information about cells, imports, and defined symbols.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
notebook_path: Path to notebook
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Dict with notebook structure information
|
|
411
|
+
"""
|
|
412
|
+
notebook = self._read_notebook(notebook_path)
|
|
413
|
+
if not notebook:
|
|
414
|
+
return {"error": "Failed to read notebook"}
|
|
415
|
+
|
|
416
|
+
cells = notebook.get("cells", [])
|
|
417
|
+
|
|
418
|
+
code_cells = []
|
|
419
|
+
markdown_cells = []
|
|
420
|
+
imports = set()
|
|
421
|
+
definitions = set()
|
|
422
|
+
|
|
423
|
+
import_pattern = re.compile(r'^(?:import|from)\s+([\w.]+)', re.MULTILINE)
|
|
424
|
+
def_pattern = re.compile(r'^(?:def|class)\s+(\w+)', re.MULTILINE)
|
|
425
|
+
var_pattern = re.compile(r'^(\w+)\s*=', re.MULTILINE)
|
|
426
|
+
|
|
427
|
+
for idx, cell in enumerate(cells):
|
|
428
|
+
cell_type = cell.get("cell_type", "code")
|
|
429
|
+
source = cell.get("source", "")
|
|
430
|
+
if isinstance(source, list):
|
|
431
|
+
source = "".join(source)
|
|
432
|
+
|
|
433
|
+
cell_info = {
|
|
434
|
+
"index": idx,
|
|
435
|
+
"preview": source[:100] + "..." if len(source) > 100 else source,
|
|
436
|
+
"lines": len(source.split("\n")),
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if cell_type == "code":
|
|
440
|
+
code_cells.append(cell_info)
|
|
441
|
+
|
|
442
|
+
# Extract imports
|
|
443
|
+
for match in import_pattern.finditer(source):
|
|
444
|
+
imports.add(match.group(1).split(".")[0])
|
|
445
|
+
|
|
446
|
+
# Extract definitions
|
|
447
|
+
for match in def_pattern.finditer(source):
|
|
448
|
+
definitions.add(match.group(1))
|
|
449
|
+
|
|
450
|
+
# Extract variable assignments
|
|
451
|
+
for match in var_pattern.finditer(source):
|
|
452
|
+
definitions.add(match.group(1))
|
|
453
|
+
else:
|
|
454
|
+
markdown_cells.append(cell_info)
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
"notebook_path": notebook_path,
|
|
458
|
+
"total_cells": len(cells),
|
|
459
|
+
"code_cells": len(code_cells),
|
|
460
|
+
"markdown_cells": len(markdown_cells),
|
|
461
|
+
"imports": sorted(imports),
|
|
462
|
+
"definitions": sorted(definitions),
|
|
463
|
+
"code_cell_previews": code_cells[:10],
|
|
464
|
+
"markdown_cell_previews": markdown_cells[:5],
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# Singleton instance
|
|
469
|
+
_searcher_instance: Optional[NotebookSearcher] = None
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def get_notebook_searcher(workspace_root: str = ".") -> NotebookSearcher:
|
|
473
|
+
"""Get or create NotebookSearcher singleton"""
|
|
474
|
+
global _searcher_instance
|
|
475
|
+
if _searcher_instance is None:
|
|
476
|
+
_searcher_instance = NotebookSearcher(workspace_root)
|
|
477
|
+
return _searcher_instance
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangChain Middleware for Jupyter Agent
|
|
3
|
+
|
|
4
|
+
Middleware stack (execution order):
|
|
5
|
+
1. RAGMiddleware: Inject RAG context before model calls
|
|
6
|
+
2. CodeSearchMiddleware: Search workspace/notebook for relevant code
|
|
7
|
+
3. ValidationMiddleware: Validate code before execution
|
|
8
|
+
4. JupyterExecutionMiddleware: Execute code in Jupyter kernel
|
|
9
|
+
5. ErrorHandlingMiddleware: Classify errors and decide recovery strategy
|
|
10
|
+
|
|
11
|
+
Built-in middleware used:
|
|
12
|
+
- SummarizationMiddleware: Compress long conversations
|
|
13
|
+
- ModelRetryMiddleware: Retry on rate limits
|
|
14
|
+
- ToolRetryMiddleware: Retry failed tool calls
|
|
15
|
+
- ModelCallLimitMiddleware: Prevent infinite loops
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from agent_server.langchain.middleware.code_search_middleware import (
|
|
19
|
+
CodeSearchMiddleware,
|
|
20
|
+
)
|
|
21
|
+
from agent_server.langchain.middleware.error_handling_middleware import (
|
|
22
|
+
ErrorHandlingMiddleware,
|
|
23
|
+
)
|
|
24
|
+
from agent_server.langchain.middleware.jupyter_execution_middleware import (
|
|
25
|
+
JupyterExecutionMiddleware,
|
|
26
|
+
)
|
|
27
|
+
from agent_server.langchain.middleware.rag_middleware import RAGMiddleware
|
|
28
|
+
from agent_server.langchain.middleware.validation_middleware import ValidationMiddleware
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"RAGMiddleware",
|
|
32
|
+
"CodeSearchMiddleware",
|
|
33
|
+
"ValidationMiddleware",
|
|
34
|
+
"JupyterExecutionMiddleware",
|
|
35
|
+
"ErrorHandlingMiddleware",
|
|
36
|
+
]
|