hanzo-mcp 0.5.0__py3-none-any.whl → 0.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/config/settings.py +61 -0
- hanzo_mcp/tools/__init__.py +158 -12
- hanzo_mcp/tools/common/base.py +7 -2
- hanzo_mcp/tools/common/config_tool.py +396 -0
- hanzo_mcp/tools/common/stats.py +261 -0
- hanzo_mcp/tools/common/tool_disable.py +144 -0
- hanzo_mcp/tools/common/tool_enable.py +182 -0
- hanzo_mcp/tools/common/tool_list.py +263 -0
- hanzo_mcp/tools/database/__init__.py +71 -0
- hanzo_mcp/tools/database/database_manager.py +246 -0
- hanzo_mcp/tools/database/graph_add.py +257 -0
- hanzo_mcp/tools/database/graph_query.py +536 -0
- hanzo_mcp/tools/database/graph_remove.py +267 -0
- hanzo_mcp/tools/database/graph_search.py +348 -0
- hanzo_mcp/tools/database/graph_stats.py +345 -0
- hanzo_mcp/tools/database/sql_query.py +229 -0
- hanzo_mcp/tools/database/sql_search.py +296 -0
- hanzo_mcp/tools/database/sql_stats.py +254 -0
- hanzo_mcp/tools/editor/__init__.py +11 -0
- hanzo_mcp/tools/editor/neovim_command.py +272 -0
- hanzo_mcp/tools/editor/neovim_edit.py +290 -0
- hanzo_mcp/tools/editor/neovim_session.py +356 -0
- hanzo_mcp/tools/filesystem/__init__.py +20 -1
- hanzo_mcp/tools/filesystem/batch_search.py +812 -0
- hanzo_mcp/tools/filesystem/find_files.py +348 -0
- hanzo_mcp/tools/filesystem/git_search.py +505 -0
- hanzo_mcp/tools/llm/__init__.py +27 -0
- hanzo_mcp/tools/llm/consensus_tool.py +351 -0
- hanzo_mcp/tools/llm/llm_manage.py +413 -0
- hanzo_mcp/tools/llm/llm_tool.py +346 -0
- hanzo_mcp/tools/llm/provider_tools.py +412 -0
- hanzo_mcp/tools/mcp/__init__.py +11 -0
- hanzo_mcp/tools/mcp/mcp_add.py +263 -0
- hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
- hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
- hanzo_mcp/tools/shell/__init__.py +27 -7
- hanzo_mcp/tools/shell/logs.py +265 -0
- hanzo_mcp/tools/shell/npx.py +194 -0
- hanzo_mcp/tools/shell/npx_background.py +254 -0
- hanzo_mcp/tools/shell/pkill.py +262 -0
- hanzo_mcp/tools/shell/processes.py +279 -0
- hanzo_mcp/tools/shell/run_background.py +326 -0
- hanzo_mcp/tools/shell/uvx.py +187 -0
- hanzo_mcp/tools/shell/uvx_background.py +249 -0
- hanzo_mcp/tools/vector/__init__.py +21 -12
- hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
- hanzo_mcp/tools/vector/git_ingester.py +485 -0
- hanzo_mcp/tools/vector/index_tool.py +358 -0
- hanzo_mcp/tools/vector/infinity_store.py +465 -1
- hanzo_mcp/tools/vector/mock_infinity.py +162 -0
- hanzo_mcp/tools/vector/vector_index.py +7 -6
- hanzo_mcp/tools/vector/vector_search.py +22 -7
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +68 -20
- hanzo_mcp-0.5.2.dist-info/RECORD +106 -0
- hanzo_mcp-0.5.0.dist-info/RECORD +0 -63
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""AST analysis and symbol extraction for code understanding."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Any, Optional, Set, Tuple
|
|
7
|
+
from dataclasses import dataclass, asdict
|
|
8
|
+
import hashlib
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import tree_sitter_python as tspython
|
|
12
|
+
import tree_sitter
|
|
13
|
+
TREE_SITTER_AVAILABLE = True
|
|
14
|
+
except ImportError:
|
|
15
|
+
TREE_SITTER_AVAILABLE = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Symbol:
|
|
20
|
+
"""Represents a code symbol (function, class, variable, etc.)."""
|
|
21
|
+
name: str
|
|
22
|
+
type: str # function, class, variable, import, etc.
|
|
23
|
+
file_path: str
|
|
24
|
+
line_start: int
|
|
25
|
+
line_end: int
|
|
26
|
+
column_start: int
|
|
27
|
+
column_end: int
|
|
28
|
+
scope: str # global, class, function
|
|
29
|
+
parent: Optional[str] = None # parent class/function
|
|
30
|
+
docstring: Optional[str] = None
|
|
31
|
+
signature: Optional[str] = None
|
|
32
|
+
references: List[str] = None # Files that reference this symbol
|
|
33
|
+
|
|
34
|
+
def __post_init__(self):
|
|
35
|
+
if self.references is None:
|
|
36
|
+
self.references = []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ASTNode:
|
|
41
|
+
"""Represents an AST node with metadata."""
|
|
42
|
+
type: str
|
|
43
|
+
name: Optional[str]
|
|
44
|
+
line_start: int
|
|
45
|
+
line_end: int
|
|
46
|
+
column_start: int
|
|
47
|
+
column_end: int
|
|
48
|
+
children: List['ASTNode'] = None
|
|
49
|
+
parent: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
def __post_init__(self):
|
|
52
|
+
if self.children is None:
|
|
53
|
+
self.children = []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class FileAST:
|
|
58
|
+
"""Complete AST representation of a file."""
|
|
59
|
+
file_path: str
|
|
60
|
+
file_hash: str
|
|
61
|
+
language: str
|
|
62
|
+
symbols: List[Symbol]
|
|
63
|
+
ast_nodes: List[ASTNode]
|
|
64
|
+
imports: List[str]
|
|
65
|
+
exports: List[str]
|
|
66
|
+
dependencies: List[str] # Files this file depends on
|
|
67
|
+
|
|
68
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
69
|
+
"""Convert to dictionary for storage."""
|
|
70
|
+
return {
|
|
71
|
+
"file_path": self.file_path,
|
|
72
|
+
"file_hash": self.file_hash,
|
|
73
|
+
"language": self.language,
|
|
74
|
+
"symbols": [asdict(s) for s in self.symbols],
|
|
75
|
+
"ast_nodes": [asdict(n) for n in self.ast_nodes],
|
|
76
|
+
"imports": self.imports,
|
|
77
|
+
"exports": self.exports,
|
|
78
|
+
"dependencies": self.dependencies,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ASTAnalyzer:
|
|
83
|
+
"""Analyzes code files and extracts AST information and symbols."""
|
|
84
|
+
|
|
85
|
+
def __init__(self):
|
|
86
|
+
"""Initialize the AST analyzer."""
|
|
87
|
+
self.parsers = {}
|
|
88
|
+
self._setup_parsers()
|
|
89
|
+
|
|
90
|
+
def _setup_parsers(self):
|
|
91
|
+
"""Set up tree-sitter parsers for different languages."""
|
|
92
|
+
if TREE_SITTER_AVAILABLE:
|
|
93
|
+
try:
|
|
94
|
+
# Python parser
|
|
95
|
+
self.parsers['python'] = tree_sitter.Language(tspython.language())
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"Warning: Could not initialize Python parser: {e}")
|
|
98
|
+
|
|
99
|
+
def analyze_file(self, file_path: str) -> Optional[FileAST]:
|
|
100
|
+
"""Analyze a file and extract AST information and symbols.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
file_path: Path to the file to analyze
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
FileAST object with extracted information, or None if analysis fails
|
|
107
|
+
"""
|
|
108
|
+
path = Path(file_path)
|
|
109
|
+
if not path.exists():
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
# Determine language
|
|
113
|
+
language = self._detect_language(path)
|
|
114
|
+
if not language:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# Read file content
|
|
119
|
+
content = path.read_text(encoding='utf-8')
|
|
120
|
+
file_hash = hashlib.sha256(content.encode()).hexdigest()
|
|
121
|
+
|
|
122
|
+
# Extract symbols and AST
|
|
123
|
+
if language == 'python':
|
|
124
|
+
return self._analyze_python_file(file_path, content, file_hash)
|
|
125
|
+
else:
|
|
126
|
+
# Generic analysis for other languages
|
|
127
|
+
return self._analyze_generic_file(file_path, content, file_hash, language)
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
print(f"Error analyzing file {file_path}: {e}")
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def _detect_language(self, path: Path) -> Optional[str]:
|
|
134
|
+
"""Detect programming language from file extension."""
|
|
135
|
+
extension = path.suffix.lower()
|
|
136
|
+
|
|
137
|
+
language_map = {
|
|
138
|
+
'.py': 'python',
|
|
139
|
+
'.js': 'javascript',
|
|
140
|
+
'.ts': 'typescript',
|
|
141
|
+
'.jsx': 'javascript',
|
|
142
|
+
'.tsx': 'typescript',
|
|
143
|
+
'.java': 'java',
|
|
144
|
+
'.cpp': 'cpp',
|
|
145
|
+
'.c': 'c',
|
|
146
|
+
'.h': 'c',
|
|
147
|
+
'.hpp': 'cpp',
|
|
148
|
+
'.rs': 'rust',
|
|
149
|
+
'.go': 'go',
|
|
150
|
+
'.rb': 'ruby',
|
|
151
|
+
'.php': 'php',
|
|
152
|
+
'.cs': 'csharp',
|
|
153
|
+
'.swift': 'swift',
|
|
154
|
+
'.kt': 'kotlin',
|
|
155
|
+
'.scala': 'scala',
|
|
156
|
+
'.clj': 'clojure',
|
|
157
|
+
'.hs': 'haskell',
|
|
158
|
+
'.ml': 'ocaml',
|
|
159
|
+
'.elm': 'elm',
|
|
160
|
+
'.dart': 'dart',
|
|
161
|
+
'.lua': 'lua',
|
|
162
|
+
'.r': 'r',
|
|
163
|
+
'.m': 'objective-c',
|
|
164
|
+
'.mm': 'objective-cpp',
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return language_map.get(extension)
|
|
168
|
+
|
|
169
|
+
def _analyze_python_file(self, file_path: str, content: str, file_hash: str) -> FileAST:
|
|
170
|
+
"""Analyze Python file using both AST and tree-sitter."""
|
|
171
|
+
symbols = []
|
|
172
|
+
ast_nodes = []
|
|
173
|
+
imports = []
|
|
174
|
+
exports = []
|
|
175
|
+
dependencies = []
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
# Parse with Python AST
|
|
179
|
+
tree = ast.parse(content)
|
|
180
|
+
|
|
181
|
+
# Extract symbols using AST visitor
|
|
182
|
+
visitor = PythonSymbolExtractor(file_path)
|
|
183
|
+
visitor.visit(tree)
|
|
184
|
+
|
|
185
|
+
symbols.extend(visitor.symbols)
|
|
186
|
+
imports.extend(visitor.imports)
|
|
187
|
+
exports.extend(visitor.exports)
|
|
188
|
+
dependencies.extend(visitor.dependencies)
|
|
189
|
+
|
|
190
|
+
# If tree-sitter is available, get more detailed AST
|
|
191
|
+
if TREE_SITTER_AVAILABLE and 'python' in self.parsers:
|
|
192
|
+
parser = tree_sitter.Parser(self.parsers['python'])
|
|
193
|
+
ts_tree = parser.parse(content.encode())
|
|
194
|
+
ast_nodes = self._extract_tree_sitter_nodes(ts_tree.root_node, content)
|
|
195
|
+
|
|
196
|
+
except SyntaxError as e:
|
|
197
|
+
print(f"Syntax error in {file_path}: {e}")
|
|
198
|
+
except Exception as e:
|
|
199
|
+
print(f"Error parsing Python file {file_path}: {e}")
|
|
200
|
+
|
|
201
|
+
return FileAST(
|
|
202
|
+
file_path=file_path,
|
|
203
|
+
file_hash=file_hash,
|
|
204
|
+
language='python',
|
|
205
|
+
symbols=symbols,
|
|
206
|
+
ast_nodes=ast_nodes,
|
|
207
|
+
imports=imports,
|
|
208
|
+
exports=exports,
|
|
209
|
+
dependencies=dependencies,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def _analyze_generic_file(self, file_path: str, content: str, file_hash: str, language: str) -> FileAST:
|
|
213
|
+
"""Generic analysis for non-Python files."""
|
|
214
|
+
# For now, just basic line-based analysis
|
|
215
|
+
# Could be enhanced with language-specific parsers
|
|
216
|
+
|
|
217
|
+
symbols = []
|
|
218
|
+
ast_nodes = []
|
|
219
|
+
imports = []
|
|
220
|
+
exports = []
|
|
221
|
+
dependencies = []
|
|
222
|
+
|
|
223
|
+
# Basic pattern matching for common constructs
|
|
224
|
+
lines = content.split('\n')
|
|
225
|
+
for i, line in enumerate(lines, 1):
|
|
226
|
+
line = line.strip()
|
|
227
|
+
|
|
228
|
+
# Basic function detection (works for many C-style languages)
|
|
229
|
+
if language in ['javascript', 'typescript', 'java', 'cpp', 'c']:
|
|
230
|
+
if 'function ' in line or line.startswith('def ') or ' function(' in line:
|
|
231
|
+
# Extract function name
|
|
232
|
+
parts = line.split()
|
|
233
|
+
for j, part in enumerate(parts):
|
|
234
|
+
if part == 'function' and j + 1 < len(parts):
|
|
235
|
+
func_name = parts[j + 1].split('(')[0]
|
|
236
|
+
symbols.append(Symbol(
|
|
237
|
+
name=func_name,
|
|
238
|
+
type='function',
|
|
239
|
+
file_path=file_path,
|
|
240
|
+
line_start=i,
|
|
241
|
+
line_end=i,
|
|
242
|
+
column_start=0,
|
|
243
|
+
column_end=len(line),
|
|
244
|
+
scope='global',
|
|
245
|
+
))
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
# Basic import detection
|
|
249
|
+
if 'import ' in line or '#include ' in line or 'require(' in line:
|
|
250
|
+
imports.append(line)
|
|
251
|
+
|
|
252
|
+
return FileAST(
|
|
253
|
+
file_path=file_path,
|
|
254
|
+
file_hash=file_hash,
|
|
255
|
+
language=language,
|
|
256
|
+
symbols=symbols,
|
|
257
|
+
ast_nodes=ast_nodes,
|
|
258
|
+
imports=imports,
|
|
259
|
+
exports=exports,
|
|
260
|
+
dependencies=dependencies,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def _extract_tree_sitter_nodes(self, node, content: str) -> List[ASTNode]:
|
|
264
|
+
"""Extract AST nodes from tree-sitter parse tree."""
|
|
265
|
+
nodes = []
|
|
266
|
+
|
|
267
|
+
def traverse(ts_node, parent_name=None):
|
|
268
|
+
node_name = None
|
|
269
|
+
|
|
270
|
+
# Try to extract node name for named nodes
|
|
271
|
+
if ts_node.type in ['function_definition', 'class_definition', 'identifier']:
|
|
272
|
+
for child in ts_node.children:
|
|
273
|
+
if child.type == 'identifier':
|
|
274
|
+
start_byte = child.start_byte
|
|
275
|
+
end_byte = child.end_byte
|
|
276
|
+
node_name = content[start_byte:end_byte]
|
|
277
|
+
break
|
|
278
|
+
|
|
279
|
+
ast_node = ASTNode(
|
|
280
|
+
type=ts_node.type,
|
|
281
|
+
name=node_name,
|
|
282
|
+
line_start=ts_node.start_point[0] + 1,
|
|
283
|
+
line_end=ts_node.end_point[0] + 1,
|
|
284
|
+
column_start=ts_node.start_point[1],
|
|
285
|
+
column_end=ts_node.end_point[1],
|
|
286
|
+
parent=parent_name,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
nodes.append(ast_node)
|
|
290
|
+
|
|
291
|
+
# Recursively process children
|
|
292
|
+
for child in ts_node.children:
|
|
293
|
+
traverse(child, node_name or parent_name)
|
|
294
|
+
|
|
295
|
+
traverse(node)
|
|
296
|
+
return nodes
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class PythonSymbolExtractor(ast.NodeVisitor):
|
|
300
|
+
"""AST visitor for extracting Python symbols."""
|
|
301
|
+
|
|
302
|
+
def __init__(self, file_path: str):
|
|
303
|
+
self.file_path = file_path
|
|
304
|
+
self.symbols = []
|
|
305
|
+
self.imports = []
|
|
306
|
+
self.exports = []
|
|
307
|
+
self.dependencies = []
|
|
308
|
+
self.scope_stack = ['global']
|
|
309
|
+
|
|
310
|
+
def visit_FunctionDef(self, node):
|
|
311
|
+
"""Visit function definitions."""
|
|
312
|
+
scope = '.'.join(self.scope_stack)
|
|
313
|
+
parent = self.scope_stack[-1] if len(self.scope_stack) > 1 else None
|
|
314
|
+
|
|
315
|
+
# Extract docstring
|
|
316
|
+
docstring = None
|
|
317
|
+
if (node.body and isinstance(node.body[0], ast.Expr) and
|
|
318
|
+
isinstance(node.body[0].value, ast.Constant) and
|
|
319
|
+
isinstance(node.body[0].value.value, str)):
|
|
320
|
+
docstring = node.body[0].value.value
|
|
321
|
+
|
|
322
|
+
# Create function signature
|
|
323
|
+
args = [arg.arg for arg in node.args.args]
|
|
324
|
+
signature = f"{node.name}({', '.join(args)})"
|
|
325
|
+
|
|
326
|
+
symbol = Symbol(
|
|
327
|
+
name=node.name,
|
|
328
|
+
type='function',
|
|
329
|
+
file_path=self.file_path,
|
|
330
|
+
line_start=node.lineno,
|
|
331
|
+
line_end=node.end_lineno or node.lineno,
|
|
332
|
+
column_start=node.col_offset,
|
|
333
|
+
column_end=node.end_col_offset or 0,
|
|
334
|
+
scope=scope,
|
|
335
|
+
parent=parent if parent != 'global' else None,
|
|
336
|
+
docstring=docstring,
|
|
337
|
+
signature=signature,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
self.symbols.append(symbol)
|
|
341
|
+
|
|
342
|
+
# Enter function scope
|
|
343
|
+
self.scope_stack.append(node.name)
|
|
344
|
+
self.generic_visit(node)
|
|
345
|
+
self.scope_stack.pop()
|
|
346
|
+
|
|
347
|
+
def visit_AsyncFunctionDef(self, node):
|
|
348
|
+
"""Visit async function definitions."""
|
|
349
|
+
self.visit_FunctionDef(node) # Same logic
|
|
350
|
+
|
|
351
|
+
def visit_ClassDef(self, node):
|
|
352
|
+
"""Visit class definitions."""
|
|
353
|
+
scope = '.'.join(self.scope_stack)
|
|
354
|
+
parent = self.scope_stack[-1] if len(self.scope_stack) > 1 else None
|
|
355
|
+
|
|
356
|
+
# Extract docstring
|
|
357
|
+
docstring = None
|
|
358
|
+
if (node.body and isinstance(node.body[0], ast.Expr) and
|
|
359
|
+
isinstance(node.body[0].value, ast.Constant) and
|
|
360
|
+
isinstance(node.body[0].value.value, str)):
|
|
361
|
+
docstring = node.body[0].value.value
|
|
362
|
+
|
|
363
|
+
# Extract base classes
|
|
364
|
+
bases = [self._get_name(base) for base in node.bases]
|
|
365
|
+
signature = f"class {node.name}({', '.join(bases)})" if bases else f"class {node.name}"
|
|
366
|
+
|
|
367
|
+
symbol = Symbol(
|
|
368
|
+
name=node.name,
|
|
369
|
+
type='class',
|
|
370
|
+
file_path=self.file_path,
|
|
371
|
+
line_start=node.lineno,
|
|
372
|
+
line_end=node.end_lineno or node.lineno,
|
|
373
|
+
column_start=node.col_offset,
|
|
374
|
+
column_end=node.end_col_offset or 0,
|
|
375
|
+
scope=scope,
|
|
376
|
+
parent=parent if parent != 'global' else None,
|
|
377
|
+
docstring=docstring,
|
|
378
|
+
signature=signature,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
self.symbols.append(symbol)
|
|
382
|
+
|
|
383
|
+
# Enter class scope
|
|
384
|
+
self.scope_stack.append(node.name)
|
|
385
|
+
self.generic_visit(node)
|
|
386
|
+
self.scope_stack.pop()
|
|
387
|
+
|
|
388
|
+
def visit_Import(self, node):
|
|
389
|
+
"""Visit import statements."""
|
|
390
|
+
for alias in node.names:
|
|
391
|
+
import_name = alias.name
|
|
392
|
+
self.imports.append(import_name)
|
|
393
|
+
if '.' not in import_name: # Top-level module
|
|
394
|
+
self.dependencies.append(import_name)
|
|
395
|
+
|
|
396
|
+
def visit_ImportFrom(self, node):
|
|
397
|
+
"""Visit from...import statements."""
|
|
398
|
+
if node.module:
|
|
399
|
+
self.imports.append(node.module)
|
|
400
|
+
if '.' not in node.module: # Top-level module
|
|
401
|
+
self.dependencies.append(node.module)
|
|
402
|
+
|
|
403
|
+
for alias in node.names:
|
|
404
|
+
if alias.name != '*':
|
|
405
|
+
import_item = f"{node.module}.{alias.name}" if node.module else alias.name
|
|
406
|
+
self.imports.append(import_item)
|
|
407
|
+
|
|
408
|
+
def visit_Assign(self, node):
|
|
409
|
+
"""Visit variable assignments."""
|
|
410
|
+
scope = '.'.join(self.scope_stack)
|
|
411
|
+
parent = self.scope_stack[-1] if len(self.scope_stack) > 1 else None
|
|
412
|
+
|
|
413
|
+
for target in node.targets:
|
|
414
|
+
if isinstance(target, ast.Name):
|
|
415
|
+
symbol = Symbol(
|
|
416
|
+
name=target.id,
|
|
417
|
+
type='variable',
|
|
418
|
+
file_path=self.file_path,
|
|
419
|
+
line_start=node.lineno,
|
|
420
|
+
line_end=node.end_lineno or node.lineno,
|
|
421
|
+
column_start=node.col_offset,
|
|
422
|
+
column_end=node.end_col_offset or 0,
|
|
423
|
+
scope=scope,
|
|
424
|
+
parent=parent if parent != 'global' else None,
|
|
425
|
+
)
|
|
426
|
+
self.symbols.append(symbol)
|
|
427
|
+
|
|
428
|
+
self.generic_visit(node)
|
|
429
|
+
|
|
430
|
+
def _get_name(self, node):
|
|
431
|
+
"""Extract name from AST node."""
|
|
432
|
+
if isinstance(node, ast.Name):
|
|
433
|
+
return node.id
|
|
434
|
+
elif isinstance(node, ast.Attribute):
|
|
435
|
+
return f"{self._get_name(node.value)}.{node.attr}"
|
|
436
|
+
elif isinstance(node, ast.Constant):
|
|
437
|
+
return str(node.value)
|
|
438
|
+
else:
|
|
439
|
+
return str(node)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def create_symbol_embedding_text(symbol: Symbol) -> str:
|
|
443
|
+
"""Create text representation of symbol for vector embedding."""
|
|
444
|
+
parts = [
|
|
445
|
+
f"Symbol: {symbol.name}",
|
|
446
|
+
f"Type: {symbol.type}",
|
|
447
|
+
f"Scope: {symbol.scope}",
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
if symbol.parent:
|
|
451
|
+
parts.append(f"Parent: {symbol.parent}")
|
|
452
|
+
|
|
453
|
+
if symbol.signature:
|
|
454
|
+
parts.append(f"Signature: {symbol.signature}")
|
|
455
|
+
|
|
456
|
+
if symbol.docstring:
|
|
457
|
+
parts.append(f"Documentation: {symbol.docstring}")
|
|
458
|
+
|
|
459
|
+
return " | ".join(parts)
|