Skip to main content
Glama
structural_view_service.py9.55 kB
"""Service for generating structural tree views of the codebase.""" import logging from typing import Dict, List, Set, Optional, Any from pathlib import Path logger = logging.getLogger(__name__) class StructuralViewService: """ Generate hierarchical tree views of codebase structure. Creates markdown trees with symbology: - +/- : collapsed/expanded state - · : leaf nodes - [N+]: child count when collapsed - ● : search match markers """ def __init__(self, db_manager): """ Initialize service. Args: db_manager: KuzuDB manager for graph queries """ self.db = db_manager def generate_tree( self, expanded_paths: Set[str], search_results: Optional[List[str]] = None, search_query: Optional[str] = None ) -> str: """ Generate complete markdown tree. Args: expanded_paths: Set of qualified names that should be expanded search_results: List of qualified names matching search (marked with ●) search_query: Current search query string (for display) Returns: Formatted markdown tree with symbology """ search_hits = set(search_results or []) # Build tree starting from language roots lines = [] # Header with search context if search_query: lines.append(f'**search query**: "{search_query}"') else: lines.append('**search query**: (none)') lines.append('') # Get language roots roots = self._get_language_roots() for root in roots: tree_lines = self._build_tree_recursive( root['qualified_name'], expanded_paths, search_hits, depth=0, is_last=True ) lines.extend(tree_lines) return '\n'.join(lines) def _get_language_roots(self) -> List[Dict[str, Any]]: """ Get language root nodes (python_root, java_root, etc.). Returns: List of language frame data """ query = """ MATCH (lang:Frame {type: 'LANGUAGE'}) RETURN lang.name AS name, lang.qualified_name AS qualified_name, size((lang)-[:Edge {type: 'CONTAINS'}]->()) AS child_count ORDER BY lang.name """ result = self.db.execute(query) if not result or not hasattr(result, 'get_as_df'): return [] df = result.get_as_df() return [ { 'name': row['name'], 'qualified_name': row['qualified_name'], 'child_count': row['child_count'] } for _, row in df.iterrows() ] def _build_tree_recursive( self, qualified_name: str, expanded_paths: Set[str], search_hits: Set[str], depth: int, is_last: bool = False, prefix: str = '' ) -> List[str]: """ Recursively build tree lines. Args: qualified_name: Node to build tree for expanded_paths: Expanded node paths search_hits: Search result qualified names depth: Current depth (for indentation) is_last: Whether this is last child of parent prefix: Line prefix for tree drawing Returns: List of formatted tree lines """ lines = [] # Get node data node = self._get_node_data(qualified_name) if not node: return lines # Check states is_expanded = qualified_name in expanded_paths is_search_hit = qualified_name in search_hits # Get children if expanded or to get count children = self._get_node_children(qualified_name) child_count = len(children) # Format this node node_line = self._format_node( node, is_expanded, is_search_hit, child_count, prefix, is_last ) lines.append(node_line) # Recurse into children if expanded if is_expanded and children: # Prepare prefix for children if depth == 0: child_prefix = '' else: child_prefix = prefix + (' ' if is_last else '│ ') for i, child in enumerate(children): is_last_child = (i == len(children) - 1) child_lines = self._build_tree_recursive( child['qualified_name'], expanded_paths, search_hits, depth + 1, is_last_child, child_prefix ) lines.extend(child_lines) return lines def _get_node_data(self, qualified_name: str) -> Optional[Dict[str, Any]]: """ Get node metadata. Args: qualified_name: Frame qualified name Returns: Node data dict or None if not found """ query = """ MATCH (node:Frame {qualified_name: $qname}) RETURN node.name AS name, node.type AS type, node.qualified_name AS qualified_name """ result = self.db.execute(query, {'qname': qualified_name}) if not result or not hasattr(result, 'get_as_df'): return None df = result.get_as_df() if df.empty: return None row = df.iloc[0] return { 'name': row['name'], 'type': row['type'], 'qualified_name': row['qualified_name'] } def _get_node_children(self, qualified_name: str) -> List[Dict[str, Any]]: """ Get children of a node. Args: qualified_name: Parent qualified name Returns: List of child node data (sorted) """ query = """ MATCH (parent:Frame {qualified_name: $qname})-[e:Edge]->(child:Frame) WHERE e.type = 'CONTAINS' AND child.type IN ['LANGUAGE', 'PACKAGE', 'CLASS', 'CALLABLE'] RETURN child.name AS name, child.type AS type, child.qualified_name AS qualified_name, size((child)-[:Edge {type: 'CONTAINS'}]->()) AS child_count ORDER BY child.type DESC, child.name """ result = self.db.execute(query, {'qname': qualified_name}) if not result or not hasattr(result, 'get_as_df'): return [] df = result.get_as_df() return [ { 'name': row['name'], 'type': row['type'], 'qualified_name': row['qualified_name'], 'child_count': row['child_count'] } for _, row in df.iterrows() ] def _format_node( self, node: Dict[str, Any], is_expanded: bool, is_search_hit: bool, child_count: int, prefix: str, is_last: bool ) -> str: """ Format single tree node with symbology. Args: node: Node data is_expanded: Whether node is expanded is_search_hit: Whether node matches search child_count: Number of children prefix: Line prefix is_last: Is last child of parent Returns: Formatted tree line """ # Tree connector if prefix == '': # Root level connector = '' else: connector = '└─' if is_last else '├─' # Expansion symbol if child_count == 0: expansion = '·' # Leaf elif is_expanded: expansion = '-' # Expanded else: expansion = '+' # Collapsed # Name with type-specific suffix name = node['name'] node_type = node['type'] # Add child count for collapsed nodes if not is_expanded and child_count > 0: count_suffix = f' [{child_count}+]' else: count_suffix = '' # Add search hit marker search_marker = ' ●' if is_search_hit else '' # Compose line with hidden metadata for state tracking qname = node['qualified_name'] line = f'{prefix}{connector}{expansion} {name}{count_suffix}{search_marker}' # Add hidden qualified name as HTML comment for state parsing line += f' <!-- {qname} -->' return line def search_frames(self, query: str) -> List[str]: """ Search for frames matching query. Args: query: Search string (matches name, qualified_name) Returns: List of qualified names matching search """ # Simple case-insensitive substring search # In future, could integrate with SearchTool search_query = """ MATCH (f:Frame) WHERE f.type IN ['PACKAGE', 'CLASS', 'CALLABLE'] AND (lower(f.name) CONTAINS $query OR lower(f.qualified_name) CONTAINS $query) RETURN f.qualified_name AS qualified_name LIMIT 100 """ result = self.db.execute( search_query, {'query': query.lower()} ) if not result or not hasattr(result, 'get_as_df'): return [] df = result.get_as_df() return df['qualified_name'].tolist()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/y3i12/nabu_nisaba'

If you have feedback or need assistance with the MCP directory API, please join our Discord server