This commit is contained in:
2025-11-16 00:50:08 +00:00
parent 82f634f249
commit cf44ea8ef9
2 changed files with 169 additions and 52 deletions
+109
View File
@@ -414,3 +414,112 @@ class LocalGraph:
result['incoming'].append(rel_info) result['incoming'].append(rel_info)
return result return result
def get_queryable_relationships(self, node_id: str) -> dict:
"""Get relationships in a format optimized for graph queries."""
if node_id not in self.graph.nodes:
return {}
node_data = self.graph.nodes[node_id]
result = {
'node_id': node_id,
'node_type': node_data.get('type', ''),
'node_name': node_data.get('name', ''),
'file': node_data.get('file', ''),
'outgoing': [],
'incoming': [],
'cross_file': [],
'same_file': []
}
node_file = node_data.get('file', '')
# Process all relationships
for neighbor_id in self.graph.successors(node_id):
if neighbor_id in self.graph.nodes:
edge_data = self.graph.edges[node_id, neighbor_id]
neighbor_data = self.graph.nodes[neighbor_id]
rel_info = {
'type': edge_data.get('type', 'related'),
'target_id': neighbor_id,
'target_name': neighbor_data.get('name', ''),
'target_type': neighbor_data.get('type', ''),
'target_file': neighbor_data.get('file', '')
}
result['outgoing'].append(rel_info)
# Classify by file relationship
if neighbor_data.get('file') and neighbor_data.get('file') != node_file:
result['cross_file'].append(rel_info)
else:
result['same_file'].append(rel_info)
# Reverse relationships
for predecessor_id in self.graph.predecessors(node_id):
if predecessor_id in self.graph.nodes:
edge_data = self.graph.edges[predecessor_id, node_id]
predecessor_data = self.graph.nodes[predecessor_id]
rel_info = {
'type': edge_data.get('type', 'related'),
'source_id': predecessor_id,
'source_name': predecessor_data.get('name', ''),
'source_type': predecessor_data.get('type', ''),
'source_file': predecessor_data.get('file', '')
}
result['incoming'].append(rel_info)
return result
def classify_relationships(self, node_id):
node_data = self.graph.nodes[node_id]
node_file = node_data.get('file', '')
result = {
'outgoing': [],
'incoming': [],
'same_file': [],
'cross_file': []
}
for neighbor_id in self.graph.successors(node_id):
if neighbor_id in self.graph.nodes:
edge_data = self.graph.edges[node_id, neighbor_id]
neighbor_data = self.graph.nodes[neighbor_id]
rel_info = {
'type': edge_data.get('type', 'related'),
'target_id': neighbor_id,
'target_name': neighbor_data.get('name', ''),
'target_type': neighbor_data.get('type', ''),
'target_file': neighbor_data.get('file', '')
}
result['outgoing'].append(rel_info)
# Classify by file relationship
if neighbor_data.get('file') and neighbor_data.get('file') != node_file:
result['cross_file'].append(rel_info)
else:
result['same_file'].append(rel_info)
# Reverse relationships
for predecessor_id in self.graph.predecessors(node_id):
if predecessor_id in self.graph.nodes:
edge_data = self.graph.edges[predecessor_id, node_id]
predecessor_data = self.graph.nodes[predecessor_id]
rel_info = {
'type': edge_data.get('type', 'related'),
'source_id': predecessor_id,
'source_name': predecessor_data.get('name', ''),
'source_type': predecessor_data.get('type', ''),
'source_file': predecessor_data.get('file', '')
}
result['incoming'].append(rel_info)
return result
+59 -51
View File
@@ -4999,76 +4999,79 @@ def _get_result_graph_context(meta: dict, graph: Optional[LocalGraph] = None) ->
if graph is None or graph.graph.number_of_nodes() == 0: if graph is None or graph.graph.number_of_nodes() == 0:
return {} return {}
context = {} _format_enhanced_results = {}
file_path = meta.get('file', '') file_path = meta.get('file', '')
entity_name = meta.get('name', '') entity_name = meta.get('name', '')
entity_type = meta.get('type', '') entity_type = meta.get('type', '')
language = meta.get('language', '') language = meta.get('language', '')
# Build node ID using the same logic as graph building # Build node ID using the same logic as graph building
node_attrs = { node_id = _build_node_id(language, entity_type, file_path, entity_name, meta)
"lang": language,
"type": entity_type,
"file": file_path,
"name": entity_name
}
node_id = _build_node_id(node_attrs)
if node_id and node_id in graph.graph: if node_id and node_id in graph.graph:
context['node_id'] = node_id context['node_id'] = node_id
# Get comprehensive relationships for graph queries # Get ALL relationships for comprehensive graph queries
cross_file_out, cross_file_in = graph.get_cross_file_relationships(node_id, file_path) cross_file_out = []
cross_file_in = []
# Format for graph queries: "node_id:relationship_type"
if cross_file_out:
context['cross_file'] = []
for rel in cross_file_out[:8]: # Limit to most important
# Extract target node info for graph queries
parts = rel.split('@')
relationship = parts[0]
target_file = parts[1] if len(parts) > 1 else ""
context['cross_file'].append(f"{relationship}->{target_file}")
if cross_file_in:
context['used_by'] = []
for rel in cross_file_in[:6]: # Limit to most important
parts = rel.split('@')
relationship = parts[0]
source_file = parts[1] if len(parts) > 1 else ""
context['used_by'].append(f"{relationship}<-{source_file}")
# Same-file relationships for local graph exploration
same_file_rels = [] same_file_rels = []
# Outgoing relationships
for neighbor_id in graph.graph.successors(node_id): for neighbor_id in graph.graph.successors(node_id):
if neighbor_id in graph.graph.nodes: if neighbor_id in graph.graph.nodes:
neighbor_file = graph.graph.nodes[neighbor_id].get('file', '')
if neighbor_file == file_path:
edge_data = graph.graph.edges[node_id, neighbor_id] edge_data = graph.graph.edges[node_id, neighbor_id]
rel_type = edge_data.get('type', 'related') neighbor_data = graph.graph.nodes[neighbor_id]
neighbor_name = graph.graph.nodes[neighbor_id].get('name', '')
neighbor_type = graph.graph.nodes[neighbor_id].get('type', '')
same_file_rels.append(f"{rel_type}:{neighbor_type}:{neighbor_name}")
rel_type = edge_data.get('type', 'related')
neighbor_name = neighbor_data.get('name', '')
neighbor_type = neighbor_data.get('type', '')
neighbor_file = neighbor_data.get('file', '')
rel_info = f"{rel_type}:{neighbor_type}:{neighbor_name}"
# Cross-file relationships (most valuable for queries)
if neighbor_file and neighbor_file != file_path:
cross_file_out.append(f"{rel_info}@{neighbor_file}")
else:
same_file_rels.append(rel_info)
# Incoming relationships (reverse dependencies)
for predecessor_id in graph.graph.predecessors(node_id):
if predecessor_id in graph.graph.nodes:
edge_data = graph.graph.edges[predecessor_id, node_id]
predecessor_data = graph.graph.nodes[predecessor_id]
rel_type = edge_data.get('type', 'related')
pred_name = predecessor_data.get('name', '')
pred_type = predecessor_data.get('type', '')
pred_file = predecessor_data.get('file', '')
if pred_file and pred_file != file_path:
cross_file_in.append(f"{rel_type}:{pred_type}:{pred_name}@{pred_file}")
# Store in context for TOON format
if cross_file_out:
context['cross_file'] = cross_file_out[:6] # Top 6 cross-file deps
if cross_file_in:
context['used_by'] = cross_file_in[:4] # Top 4 reverse deps
if same_file_rels: if same_file_rels:
context['same_file'] = same_file_rels[:10] context['same_file'] = same_file_rels[:8] # Top 8 same-file rels
return context return context
def _build_node_id(attrs: dict) -> str: def _build_node_id(lang: str, entity_type: str, file_path: str, entity_name: str, meta: dict) -> str:
"""Build node ID using the same logic as graph building.""" """Build node ID using the same logic as graph building."""
lang = attrs.get("lang", "unknown") if not entity_name or not file_path or not lang:
node_type = attrs.get("type", "Symbol") return None
name = attrs.get("name", "unknown")
fpath = attrs.get("file", "")
# Handle special cases # Handle special cases to match graph building logic
if node_type == "component" and lang == "svelte": if entity_type in ["method", "function"] and meta.get("class"):
return f"svelte::component::{fpath}::{name}" class_name = meta.get("class")
elif node_type in ["method", "function"] and attrs.get("class_name"): return f"{lang}::{entity_type}::{file_path}::{class_name}.{entity_name}"
return f"{lang}::{node_type}::{fpath}::{attrs['class_name']}.{name}" elif entity_type == "component" and lang == "svelte":
return f"svelte::component::{file_path}::{entity_name}"
else: else:
return f"{lang}::{node_type}::{fpath}::{name}" return f"{lang}::{entity_type}::{file_path}::{entity_name}"
def _format_enhanced_results(results: List[Dict], query: str, result_type: str) -> str: def _format_enhanced_results(results: List[Dict], query: str, result_type: str) -> str:
"""Format results in TOON (Token-Oriented Object Notation) for LLM consumption.""" """Format results in TOON (Token-Oriented Object Notation) for LLM consumption."""
@@ -5091,23 +5094,28 @@ def _format_enhanced_results(results: List[Dict], query: str, result_type: str)
content = result.get('content', '') content = result.get('content', '')
graph_context = result.get('graph_context', {}) graph_context = result.get('graph_context', {})
# Build compact graph context string # Build compact graph context string for queries
graph_parts = [] graph_parts = []
# Node ID for direct graph queries
if graph_context.get('node_id'): if graph_context.get('node_id'):
graph_parts.append(f"node:{graph_context['node_id']}") graph_parts.append(f"node:{graph_context['node_id']}")
# Cross-file dependencies for import/call queries
if graph_context.get('cross_file'): if graph_context.get('cross_file'):
graph_parts.append(f"xfile:{'|'.join(graph_context['cross_file'][:4])}") graph_parts.append(f"xfile:{'|'.join(graph_context['cross_file'][:4])}")
# Reverse dependencies for "used by" queries
if graph_context.get('used_by'): if graph_context.get('used_by'):
graph_parts.append(f"used:{'|'.join(graph_context['used_by'][:3])}") graph_parts.append(f"used:{'|'.join(graph_context['used_by'][:3])}")
# Same-file relationships for file context queries
if graph_context.get('same_file'): if graph_context.get('same_file'):
graph_parts.append(f"same:{'|'.join(graph_context['same_file'][:5])}") graph_parts.append(f"same:{'|'.join(graph_context['same_file'][:5])}")
graph_str = ";".join(graph_parts) graph_str = ";".join(graph_parts)
# Build TOON row # Build TOON row - compact format for LLM parsing
row = [ row = [
file_path, file_path,
str(line_num), str(line_num),
@@ -5115,7 +5123,7 @@ def _format_enhanced_results(results: List[Dict], query: str, result_type: str)
entity_name, entity_name,
language, language,
f"{score:.2f}", f"{score:.2f}",
content, # No truncation - let LLM handle it content, # Full content - LLM can handle truncation if needed
graph_str graph_str
] ]