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.
Files changed (78) hide show
  1. agent_server/langchain/__init__.py +18 -0
  2. agent_server/langchain/agent.py +694 -0
  3. agent_server/langchain/executors/__init__.py +15 -0
  4. agent_server/langchain/executors/jupyter_executor.py +429 -0
  5. agent_server/langchain/executors/notebook_searcher.py +477 -0
  6. agent_server/langchain/middleware/__init__.py +36 -0
  7. agent_server/langchain/middleware/code_search_middleware.py +278 -0
  8. agent_server/langchain/middleware/error_handling_middleware.py +338 -0
  9. agent_server/langchain/middleware/jupyter_execution_middleware.py +301 -0
  10. agent_server/langchain/middleware/rag_middleware.py +227 -0
  11. agent_server/langchain/middleware/validation_middleware.py +240 -0
  12. agent_server/langchain/state.py +159 -0
  13. agent_server/langchain/tools/__init__.py +39 -0
  14. agent_server/langchain/tools/file_tools.py +279 -0
  15. agent_server/langchain/tools/jupyter_tools.py +143 -0
  16. agent_server/langchain/tools/search_tools.py +309 -0
  17. agent_server/main.py +13 -0
  18. agent_server/routers/health.py +14 -0
  19. agent_server/routers/langchain_agent.py +1368 -0
  20. {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
  21. {hdsp_jupyter_extension-2.0.1.data → hdsp_jupyter_extension-2.0.3.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
  22. 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
  23. hdsp_jupyter_extension-2.0.3.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.634cf0ae0f3592d0882f.js.map +1 -0
  24. 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
  25. hdsp_jupyter_extension-2.0.3.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.1366019c413f1d68467f.js.map +1 -0
  26. 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
  27. hdsp_jupyter_extension-2.0.3.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.b6d91b150c0800bddfa4.js.map +1 -0
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. {hdsp_jupyter_extension-2.0.1.dist-info → hdsp_jupyter_extension-2.0.3.dist-info}/METADATA +6 -1
  35. {hdsp_jupyter_extension-2.0.1.dist-info → hdsp_jupyter_extension-2.0.3.dist-info}/RECORD +66 -49
  36. jupyter_ext/_version.py +1 -1
  37. jupyter_ext/handlers.py +126 -1
  38. jupyter_ext/labextension/build_log.json +1 -1
  39. jupyter_ext/labextension/package.json +2 -2
  40. jupyter_ext/labextension/static/{frontend_styles_index_js.2607ff74c74acfa83158.js → frontend_styles_index_js.634cf0ae0f3592d0882f.js} +408 -4
  41. jupyter_ext/labextension/static/frontend_styles_index_js.634cf0ae0f3592d0882f.js.map +1 -0
  42. jupyter_ext/labextension/static/{lib_index_js.622c1a5918b3aafb2315.js → lib_index_js.1366019c413f1d68467f.js} +753 -65
  43. jupyter_ext/labextension/static/lib_index_js.1366019c413f1d68467f.js.map +1 -0
  44. jupyter_ext/labextension/static/{remoteEntry.729f933de01ad5620730.js → remoteEntry.b6d91b150c0800bddfa4.js} +8 -8
  45. jupyter_ext/labextension/static/remoteEntry.b6d91b150c0800bddfa4.js.map +1 -0
  46. 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
  47. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +1 -0
  48. 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
  49. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +1 -0
  50. 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
  51. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +1 -0
  52. hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2607ff74c74acfa83158.js.map +0 -1
  53. hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.622c1a5918b3aafb2315.js.map +0 -1
  54. hdsp_jupyter_extension-2.0.1.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.729f933de01ad5620730.js.map +0 -1
  55. 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
  56. 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
  57. 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
  58. jupyter_ext/labextension/static/frontend_styles_index_js.2607ff74c74acfa83158.js.map +0 -1
  59. jupyter_ext/labextension/static/lib_index_js.622c1a5918b3aafb2315.js.map +0 -1
  60. jupyter_ext/labextension/static/remoteEntry.729f933de01ad5620730.js.map +0 -1
  61. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +0 -1
  62. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
  63. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
  64. {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
  65. {hdsp_jupyter_extension-2.0.1.data → hdsp_jupyter_extension-2.0.3.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {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
  72. {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
  73. {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
  74. {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
  75. {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
  76. {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
  77. {hdsp_jupyter_extension-2.0.1.dist-info → hdsp_jupyter_extension-2.0.3.dist-info}/WHEEL +0 -0
  78. {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
+ ]