Commit
·
547246f
1
Parent(s):
a0b7cd2
add pagination + better graph stats
Browse files- gradio_mcp_space.py +260 -173
gradio_mcp_space.py
CHANGED
|
@@ -245,7 +245,7 @@ Incoming Edges ({len(incoming)}):
|
|
| 245 |
|
| 246 |
|
| 247 |
@observe(as_type="tool")
|
| 248 |
-
def search_nodes(query: str, limit: int = 10) -> str:
|
| 249 |
"""
|
| 250 |
Search for chunk nodes in the knowledge graph by query string.
|
| 251 |
|
|
@@ -253,7 +253,8 @@ def search_nodes(query: str, limit: int = 10) -> str:
|
|
| 253 |
|
| 254 |
Args:
|
| 255 |
query: The search string to match against code index
|
| 256 |
-
limit: Maximum number of results to return (default: 10)
|
|
|
|
| 257 |
|
| 258 |
Returns:
|
| 259 |
str: A formatted string with search results
|
|
@@ -269,39 +270,69 @@ def search_nodes(query: str, limit: int = 10) -> str:
|
|
| 269 |
except ValueError:
|
| 270 |
return f"Error: 'limit' must be an integer, got '{limit}'"
|
| 271 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
if limit <= 0:
|
| 273 |
return "Error: limit must be a positive integer"
|
|
|
|
|
|
|
| 274 |
|
| 275 |
-
results
|
|
|
|
|
|
|
| 276 |
metadatas = results.get("metadatas", [[]])[0]
|
| 277 |
|
| 278 |
if not metadatas:
|
| 279 |
return f"No results found for '{query}'."
|
| 280 |
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
| 283 |
|
| 284 |
-
for i, res in enumerate(
|
| 285 |
result += f"{i}. ID: {res.get('id', 'N/A')}\n"
|
| 286 |
content = res.get('content', '')
|
| 287 |
if content:
|
| 288 |
result += f" Content: {content}\n"
|
| 289 |
result += "\n"
|
| 290 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
return result
|
| 292 |
except Exception as e:
|
| 293 |
return f"Error: {str(e)}"
|
| 294 |
|
| 295 |
-
|
| 296 |
@observe(as_type="tool")
|
| 297 |
def get_graph_stats() -> str:
|
| 298 |
"""
|
| 299 |
-
Get
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
-
|
| 302 |
|
| 303 |
Returns:
|
| 304 |
-
str: A formatted string with graph statistics
|
| 305 |
"""
|
| 306 |
if knowledge_graph is None:
|
| 307 |
return "Error: Knowledge graph not initialized"
|
|
@@ -311,30 +342,92 @@ def get_graph_stats() -> str:
|
|
| 311 |
num_nodes = g.number_of_nodes()
|
| 312 |
num_edges = g.number_of_edges()
|
| 313 |
|
|
|
|
| 314 |
node_types = {}
|
|
|
|
|
|
|
| 315 |
for _, node_attrs in g.nodes(data=True):
|
| 316 |
node_type = getattr(node_attrs['data'], 'node_type', 'Unknown')
|
| 317 |
node_types[node_type] = node_types.get(node_type, 0) + 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
|
|
|
|
| 319 |
edge_relations = {}
|
| 320 |
for _, _, attrs in g.edges(data=True):
|
| 321 |
relation = attrs.get('relation', 'Unknown')
|
| 322 |
edge_relations[relation] = edge_relations.get(relation, 0) + 1
|
| 323 |
|
|
|
|
| 324 |
result = f"""Knowledge Graph Statistics:
|
| 325 |
-
|
| 326 |
|
| 327 |
-
|
| 328 |
-
Total
|
|
|
|
| 329 |
|
| 330 |
-
|
|
|
|
|
|
|
| 331 |
"""
|
|
|
|
|
|
|
| 332 |
for ntype, count in sorted(node_types.items(), key=lambda x: x[1], reverse=True):
|
| 333 |
-
result += f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
|
| 335 |
-
result += "
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
for relation, count in sorted(edge_relations.items(), key=lambda x: x[1], reverse=True):
|
| 337 |
-
result += f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
|
| 339 |
return result
|
| 340 |
except Exception as e:
|
|
@@ -541,7 +634,7 @@ def go_to_definition(entity_name: str) -> str:
|
|
| 541 |
|
| 542 |
|
| 543 |
@observe(as_type="tool")
|
| 544 |
-
def find_usages(entity_name: str, limit: int = 20) -> str:
|
| 545 |
"""
|
| 546 |
Retrieve all usages or calls of an entity in the codebase.
|
| 547 |
|
|
@@ -549,7 +642,8 @@ def find_usages(entity_name: str, limit: int = 20) -> str:
|
|
| 549 |
|
| 550 |
Args:
|
| 551 |
entity_name: The name of the entity to retrieve usages for
|
| 552 |
-
limit: Maximum number of usages to return (default: 20)
|
|
|
|
| 553 |
|
| 554 |
Returns:
|
| 555 |
str: A formatted string with usage locations
|
|
@@ -565,11 +659,20 @@ def find_usages(entity_name: str, limit: int = 20) -> str:
|
|
| 565 |
except ValueError:
|
| 566 |
return f"Error: 'limit' must be an integer, got '{limit}'"
|
| 567 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
if entity_name not in knowledge_graph.entities:
|
| 569 |
return f"Error: Entity '{entity_name}' not found in knowledge graph"
|
| 570 |
|
| 571 |
if limit <= 0:
|
| 572 |
return "Error: limit must be a positive integer"
|
|
|
|
|
|
|
| 573 |
|
| 574 |
entity_info = knowledge_graph.entities[entity_name]
|
| 575 |
calling_chunks = entity_info.get('calling_chunk_ids', [])
|
|
@@ -577,17 +680,28 @@ def find_usages(entity_name: str, limit: int = 20) -> str:
|
|
| 577 |
if not calling_chunks:
|
| 578 |
return f"Entity '{entity_name}' found but no usages identified."
|
| 579 |
|
| 580 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
| 582 |
|
| 583 |
-
for i, chunk_id in enumerate(
|
| 584 |
if chunk_id in knowledge_graph.graph:
|
| 585 |
chunk = knowledge_graph.graph.nodes[chunk_id]['data']
|
| 586 |
result += f"{i}. {chunk.path} (chunk {chunk.order_in_file})\n"
|
| 587 |
result += f" Content:\n{chunk.content}\n\n"
|
| 588 |
|
| 589 |
-
|
| 590 |
-
|
|
|
|
| 591 |
|
| 592 |
return result
|
| 593 |
except Exception as e:
|
|
@@ -649,7 +763,7 @@ def get_file_structure(file_path: str) -> str:
|
|
| 649 |
|
| 650 |
|
| 651 |
@observe(as_type="tool")
|
| 652 |
-
def get_related_chunks(chunk_id: str, relation_type: str = "calls") -> str:
|
| 653 |
"""
|
| 654 |
Retrieve chunks related to a given chunk by a specific relationship.
|
| 655 |
|
|
@@ -658,6 +772,8 @@ def get_related_chunks(chunk_id: str, relation_type: str = "calls") -> str:
|
|
| 658 |
Args:
|
| 659 |
chunk_id: The ID of the chunk to retrieve related chunks for
|
| 660 |
relation_type: The type of relationship to filter by (default: 'calls')
|
|
|
|
|
|
|
| 661 |
|
| 662 |
Returns:
|
| 663 |
str: A formatted string with related chunks
|
|
@@ -669,6 +785,24 @@ def get_related_chunks(chunk_id: str, relation_type: str = "calls") -> str:
|
|
| 669 |
if chunk_id not in knowledge_graph.graph:
|
| 670 |
return f"Error: Chunk '{chunk_id}' not found in knowledge graph"
|
| 671 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
related = []
|
| 673 |
if relation_type == "" or relation_type == "all":
|
| 674 |
# Get all outgoing edges regardless of relation type
|
|
@@ -692,18 +826,29 @@ def get_related_chunks(chunk_id: str, relation_type: str = "calls") -> str:
|
|
| 692 |
if not related:
|
| 693 |
return f"No chunks found with '{relation_type}' relationship from '{chunk_id}'"
|
| 694 |
|
| 695 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
| 697 |
|
| 698 |
-
for i, chunk in enumerate(
|
| 699 |
result += f"{i}. {chunk['id']}\n"
|
| 700 |
result += f" File: {chunk['file_path']}\n"
|
| 701 |
if chunk['entity_name']:
|
| 702 |
result += f" Entity: {chunk['entity_name']}\n"
|
| 703 |
result += "\n"
|
| 704 |
|
| 705 |
-
|
| 706 |
-
|
|
|
|
| 707 |
|
| 708 |
return result
|
| 709 |
except Exception as e:
|
|
@@ -1057,9 +1202,9 @@ def search_by_type_and_name(node_type: str, name_query: str, limit: int = 10, pa
|
|
| 1057 |
except ValueError:
|
| 1058 |
return f"Error: 'page' must be an integer, got '{page}'"
|
| 1059 |
|
| 1060 |
-
# Convert
|
| 1061 |
-
if isinstance(
|
| 1062 |
-
|
| 1063 |
|
| 1064 |
if limit <= 0:
|
| 1065 |
return "Error: limit must be a positive integer"
|
|
@@ -1070,13 +1215,13 @@ def search_by_type_and_name(node_type: str, name_query: str, limit: int = 10, pa
|
|
| 1070 |
matches = []
|
| 1071 |
query_lower = name_query.lower()
|
| 1072 |
|
| 1073 |
-
# Build regex pattern for
|
| 1074 |
# This will match names containing all characters of the query in order
|
| 1075 |
-
if
|
| 1076 |
# Create pattern that matches query as substring or with characters spread out
|
| 1077 |
# e.g., "Embed" matches "Embedding", "BertEmbeddings", "EmbedLayer"
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
|
| 1081 |
for nid, n in g.nodes(data=True):
|
| 1082 |
node = n['data']
|
|
@@ -1087,9 +1232,9 @@ def search_by_type_and_name(node_type: str, name_query: str, limit: int = 10, pa
|
|
| 1087 |
|
| 1088 |
# Check if name matches the query
|
| 1089 |
name_matches = False
|
| 1090 |
-
if
|
| 1091 |
-
#
|
| 1092 |
-
if query_lower in node_name.lower() or
|
| 1093 |
name_matches = True
|
| 1094 |
else:
|
| 1095 |
# Exact substring match
|
|
@@ -1371,7 +1516,7 @@ def get_subgraph(node_id: str, depth: int = 2, edge_types: Optional[str] = None)
|
|
| 1371 |
|
| 1372 |
|
| 1373 |
@observe(as_type="tool")
|
| 1374 |
-
def list_files_in_directory(directory_path: str = "", pattern: str = "*", recursive: bool = True, limit: int = 50) -> str:
|
| 1375 |
"""
|
| 1376 |
List files in a directory with optional glob pattern matching.
|
| 1377 |
|
|
@@ -1382,7 +1527,8 @@ def list_files_in_directory(directory_path: str = "", pattern: str = "*", recurs
|
|
| 1382 |
directory_path: Path to the directory to list (empty string for root/all files)
|
| 1383 |
pattern: Glob pattern to filter files (e.g., '*.py', 'test_*.py', '**/*.js')
|
| 1384 |
recursive: Whether to search recursively in subdirectories (default: True)
|
| 1385 |
-
limit: Maximum number of files to return (default: 50)
|
|
|
|
| 1386 |
|
| 1387 |
Returns:
|
| 1388 |
str: A formatted string with matching files
|
|
@@ -1398,6 +1544,18 @@ def list_files_in_directory(directory_path: str = "", pattern: str = "*", recurs
|
|
| 1398 |
except ValueError:
|
| 1399 |
return f"Error: 'limit' must be an integer, got '{limit}'"
|
| 1400 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1401 |
# Convert recursive to bool if it's a string
|
| 1402 |
if isinstance(recursive, str):
|
| 1403 |
recursive = recursive.lower() in ('true', '1', 'yes')
|
|
@@ -1445,9 +1603,6 @@ def list_files_in_directory(directory_path: str = "", pattern: str = "*", recurs
|
|
| 1445 |
'language': language,
|
| 1446 |
'entity_count': len(declared_entities)
|
| 1447 |
})
|
| 1448 |
-
|
| 1449 |
-
if len(matching_files) >= limit:
|
| 1450 |
-
break
|
| 1451 |
|
| 1452 |
# Sort by path for consistent ordering
|
| 1453 |
matching_files.sort(key=lambda x: x['path'])
|
|
@@ -1457,118 +1612,31 @@ def list_files_in_directory(directory_path: str = "", pattern: str = "*", recurs
|
|
| 1457 |
pattern_desc = f" matching '{pattern}'" if pattern and pattern != '*' else ""
|
| 1458 |
return f"No files found{filter_desc}{pattern_desc}."
|
| 1459 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1460 |
result = f"Files"
|
| 1461 |
if directory_path:
|
| 1462 |
result += f" in '{directory_path}'"
|
| 1463 |
if pattern and pattern != '*':
|
| 1464 |
result += f" matching '{pattern}'"
|
| 1465 |
-
result += f" ({
|
| 1466 |
result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
| 1467 |
|
| 1468 |
-
for i, f in enumerate(
|
| 1469 |
result += f"{i}. {f['path']}\n"
|
| 1470 |
result += f" Language: {f['language']}, Entities: {f['entity_count']}\n\n"
|
| 1471 |
|
| 1472 |
-
|
| 1473 |
-
|
| 1474 |
-
|
| 1475 |
-
|
| 1476 |
-
|
| 1477 |
-
@observe(as_type="tool")
|
| 1478 |
-
def find_classes_inheriting_from(base_class_name: str, limit: int = 20) -> str:
|
| 1479 |
-
"""
|
| 1480 |
-
Retrieve all classes that inherit from a given base class.
|
| 1481 |
-
|
| 1482 |
-
Searches the knowledge graph for class entities that have the specified
|
| 1483 |
-
base class in their inheritance chain.
|
| 1484 |
-
|
| 1485 |
-
Args:
|
| 1486 |
-
base_class_name: The name of the base class to retrieve subclasses of
|
| 1487 |
-
limit: Maximum number of results to return (default: 20)
|
| 1488 |
-
|
| 1489 |
-
Returns:
|
| 1490 |
-
str: A formatted string with classes inheriting from the base class
|
| 1491 |
-
"""
|
| 1492 |
-
if knowledge_graph is None:
|
| 1493 |
-
return "Error: Knowledge graph not initialized"
|
| 1494 |
-
|
| 1495 |
-
try:
|
| 1496 |
-
# Convert limit to int if it's a string
|
| 1497 |
-
if isinstance(limit, str):
|
| 1498 |
-
try:
|
| 1499 |
-
limit = int(limit)
|
| 1500 |
-
except ValueError:
|
| 1501 |
-
return f"Error: 'limit' must be an integer, got '{limit}'"
|
| 1502 |
-
|
| 1503 |
-
g = knowledge_graph.graph
|
| 1504 |
-
inheriting_classes = []
|
| 1505 |
-
base_lower = base_class_name.lower()
|
| 1506 |
-
|
| 1507 |
-
# First, find all class entities
|
| 1508 |
-
for nid, n in g.nodes(data=True):
|
| 1509 |
-
node = n['data']
|
| 1510 |
-
node_type = getattr(node, 'node_type', None)
|
| 1511 |
-
entity_type = getattr(node, 'entity_type', '')
|
| 1512 |
-
|
| 1513 |
-
if node_type != 'entity' or entity_type.lower() != 'class':
|
| 1514 |
-
continue
|
| 1515 |
-
|
| 1516 |
-
class_name = getattr(node, 'name', '')
|
| 1517 |
-
|
| 1518 |
-
# Check if this class has relationships indicating inheritance
|
| 1519 |
-
# Look for 'inherits', 'extends', or similar relationships
|
| 1520 |
-
for _, target, edge_data in g.out_edges(nid, data=True):
|
| 1521 |
-
relation = edge_data.get('relation', '').lower()
|
| 1522 |
-
target_node = g.nodes[target]['data']
|
| 1523 |
-
target_name = getattr(target_node, 'name', '')
|
| 1524 |
-
|
| 1525 |
-
if relation in ('inherits', 'extends', 'inherits_from', 'base_class'):
|
| 1526 |
-
if target_name.lower() == base_lower or base_lower in target_name.lower():
|
| 1527 |
-
declaring_chunks = getattr(node, 'declaring_chunk_ids', [])
|
| 1528 |
-
inheriting_classes.append({
|
| 1529 |
-
'name': class_name,
|
| 1530 |
-
'id': nid,
|
| 1531 |
-
'base': target_name,
|
| 1532 |
-
'file': declaring_chunks[0] if declaring_chunks else 'Unknown'
|
| 1533 |
-
})
|
| 1534 |
-
break
|
| 1535 |
-
|
| 1536 |
-
# Also check called_entities for base class references
|
| 1537 |
-
# (Sometimes inheritance is tracked via calls relationship)
|
| 1538 |
-
called = getattr(node, 'called_entities', [])
|
| 1539 |
-
if any(base_lower in str(c).lower() for c in called):
|
| 1540 |
-
# Check if it's likely an inheritance pattern
|
| 1541 |
-
declaring_chunks = getattr(node, 'declaring_chunk_ids', [])
|
| 1542 |
-
if declaring_chunks:
|
| 1543 |
-
chunk_id = declaring_chunks[0]
|
| 1544 |
-
if chunk_id in g:
|
| 1545 |
-
chunk_node = g.nodes[chunk_id]['data']
|
| 1546 |
-
content = getattr(chunk_node, 'content', '')
|
| 1547 |
-
# Look for class definition with inheritance pattern
|
| 1548 |
-
class_pattern = rf'class\s+{re.escape(class_name)}\s*\([^)]*{re.escape(base_class_name)}'
|
| 1549 |
-
if re.search(class_pattern, content, re.IGNORECASE):
|
| 1550 |
-
if not any(c['name'] == class_name for c in inheriting_classes):
|
| 1551 |
-
inheriting_classes.append({
|
| 1552 |
-
'name': class_name,
|
| 1553 |
-
'id': nid,
|
| 1554 |
-
'base': base_class_name,
|
| 1555 |
-
'file': chunk_id
|
| 1556 |
-
})
|
| 1557 |
-
|
| 1558 |
-
if len(inheriting_classes) >= limit:
|
| 1559 |
-
break
|
| 1560 |
-
|
| 1561 |
-
if not inheriting_classes:
|
| 1562 |
-
return f"No classes found inheriting from '{base_class_name}'.\n\nTip: Try searching for the base class name in code content using search_nodes."
|
| 1563 |
-
|
| 1564 |
-
result = f"Classes inheriting from '{base_class_name}' ({len(inheriting_classes)} results):\n"
|
| 1565 |
-
result += "━━━━━━━━━━━━━━━━━━━━━━━━━���━━━━━━━━━━━━━━\n\n"
|
| 1566 |
-
|
| 1567 |
-
for i, cls in enumerate(inheriting_classes, 1):
|
| 1568 |
-
result += f"{i}. {cls['name']}\n"
|
| 1569 |
-
result += f" ID: {cls['id']}\n"
|
| 1570 |
-
result += f" Inherits from: {cls['base']}\n"
|
| 1571 |
-
result += f" Defined in: {cls['file']}\n\n"
|
| 1572 |
|
| 1573 |
return result
|
| 1574 |
except Exception as e:
|
|
@@ -1576,7 +1644,7 @@ def find_classes_inheriting_from(base_class_name: str, limit: int = 20) -> str:
|
|
| 1576 |
|
| 1577 |
|
| 1578 |
@observe(as_type="tool")
|
| 1579 |
-
def find_files_importing(module_or_entity: str, limit: int = 30) -> str:
|
| 1580 |
"""
|
| 1581 |
Retrieve all files that import a specific module or entity.
|
| 1582 |
|
|
@@ -1584,7 +1652,8 @@ def find_files_importing(module_or_entity: str, limit: int = 30) -> str:
|
|
| 1584 |
|
| 1585 |
Args:
|
| 1586 |
module_or_entity: The name of the module or entity to retrieve imports of
|
| 1587 |
-
limit: Maximum number of results to return (default: 30)
|
|
|
|
| 1588 |
|
| 1589 |
Returns:
|
| 1590 |
str: A formatted string with files that import the specified module/entity
|
|
@@ -1599,6 +1668,18 @@ def find_files_importing(module_or_entity: str, limit: int = 30) -> str:
|
|
| 1599 |
limit = int(limit)
|
| 1600 |
except ValueError:
|
| 1601 |
return f"Error: 'limit' must be an integer, got '{limit}'"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1602 |
|
| 1603 |
g = knowledge_graph.graph
|
| 1604 |
importing_files = []
|
|
@@ -1654,9 +1735,6 @@ def find_files_importing(module_or_entity: str, limit: int = 30) -> str:
|
|
| 1654 |
'match_type': 'import_statement'
|
| 1655 |
})
|
| 1656 |
break
|
| 1657 |
-
|
| 1658 |
-
if len(importing_files) >= limit:
|
| 1659 |
-
break
|
| 1660 |
|
| 1661 |
# Sort by path
|
| 1662 |
importing_files.sort(key=lambda x: x['path'])
|
|
@@ -1664,16 +1742,30 @@ def find_files_importing(module_or_entity: str, limit: int = 30) -> str:
|
|
| 1664 |
if not importing_files:
|
| 1665 |
return f"No files found importing '{module_or_entity}'.\n\nTip: Try searching for the module name in code content using search_nodes."
|
| 1666 |
|
| 1667 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1668 |
result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
| 1669 |
|
| 1670 |
-
for i, f in enumerate(
|
| 1671 |
result += f"{i}. {f['path']}\n"
|
| 1672 |
result += f" Match type: {f['match_type']}\n"
|
| 1673 |
if f['matched_entities']:
|
| 1674 |
result += f" Matched: {', '.join(f['matched_entities'][:3])}\n"
|
| 1675 |
result += "\n"
|
| 1676 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1677 |
return result
|
| 1678 |
except Exception as e:
|
| 1679 |
return f"Error: {str(e)}"
|
|
@@ -1848,11 +1940,12 @@ def create_gradio_app():
|
|
| 1848 |
with gr.Row():
|
| 1849 |
with gr.Column():
|
| 1850 |
search_query = gr.Textbox(label="Search Query", placeholder="Enter search query...")
|
| 1851 |
-
search_limit = gr.Slider(1, 50, value=10, step=1, label="
|
|
|
|
| 1852 |
search_btn = gr.Button("Search", variant="primary")
|
| 1853 |
with gr.Column():
|
| 1854 |
search_output = gr.Textbox(label="Search Results", lines=20, max_lines=30)
|
| 1855 |
-
search_btn.click(fn=search_nodes, inputs=[search_query, search_limit], outputs=search_output)
|
| 1856 |
gr.Markdown(_tool_doc_md(search_nodes))
|
| 1857 |
|
| 1858 |
with gr.Tab("📝 Node Info"):
|
|
@@ -1937,11 +2030,12 @@ def create_gradio_app():
|
|
| 1937 |
with gr.Row():
|
| 1938 |
with gr.Column():
|
| 1939 |
entity_name_usage = gr.Textbox(label="Entity Name", placeholder="Enter entity name...")
|
| 1940 |
-
usage_limit = gr.Slider(1, 50, value=20, step=1, label="
|
|
|
|
| 1941 |
usage_btn = gr.Button("Find Usages", variant="primary")
|
| 1942 |
with gr.Column():
|
| 1943 |
usage_output = gr.Textbox(label="Usages", lines=15, max_lines=25)
|
| 1944 |
-
usage_btn.click(fn=find_usages, inputs=[entity_name_usage, usage_limit], outputs=usage_output)
|
| 1945 |
gr.Markdown(_tool_doc_md(find_usages))
|
| 1946 |
|
| 1947 |
with gr.Tab("🔬 Discovery"):
|
|
@@ -1971,11 +2065,11 @@ def create_gradio_app():
|
|
| 1971 |
search_name = gr.Textbox(label="Name Contains", placeholder="Enter partial name...")
|
| 1972 |
search_limit = gr.Slider(1, 100, value=10, step=1, label="Max Results")
|
| 1973 |
search_page = gr.Slider(1, 100, value=1, step=1, label="Page")
|
| 1974 |
-
|
| 1975 |
search_type_btn = gr.Button("Search", variant="primary")
|
| 1976 |
with gr.Column():
|
| 1977 |
search_type_output = gr.Textbox(label="Results", lines=20, max_lines=30)
|
| 1978 |
-
search_type_btn.click(fn=search_by_type_and_name, inputs=[search_type, search_name, search_limit, search_page,
|
| 1979 |
gr.Markdown(_tool_doc_md(search_by_type_and_name))
|
| 1980 |
|
| 1981 |
with gr.Tab("🔗 Relationships"):
|
|
@@ -2008,10 +2102,12 @@ def create_gradio_app():
|
|
| 2008 |
with gr.Column():
|
| 2009 |
related_chunk_id = gr.Textbox(label="Chunk ID", placeholder="Enter chunk ID...")
|
| 2010 |
relation_type = gr.Dropdown(choices=["" , "calls", "contains", "declares", "uses"], label="Relation Type", value="calls")
|
|
|
|
|
|
|
| 2011 |
related_btn = gr.Button("Get Related Chunks", variant="primary")
|
| 2012 |
with gr.Column():
|
| 2013 |
related_output = gr.Textbox(label="Related Chunks", lines=20, max_lines=30)
|
| 2014 |
-
related_btn.click(fn=get_related_chunks, inputs=[related_chunk_id, relation_type], outputs=related_output)
|
| 2015 |
gr.Markdown(_tool_doc_md(get_related_chunks))
|
| 2016 |
|
| 2017 |
gr.Markdown("---")
|
|
@@ -2027,17 +2123,6 @@ def create_gradio_app():
|
|
| 2027 |
path_btn.click(fn=find_path, inputs=[path_source, path_target, path_depth], outputs=path_output)
|
| 2028 |
gr.Markdown(_tool_doc_md(find_path))
|
| 2029 |
|
| 2030 |
-
gr.Markdown("---")
|
| 2031 |
-
gr.Markdown("### Find Classes Inheriting From")
|
| 2032 |
-
with gr.Row():
|
| 2033 |
-
with gr.Column():
|
| 2034 |
-
base_class_input = gr.Textbox(label="Base Class Name", placeholder="Enter base class...")
|
| 2035 |
-
inherit_btn = gr.Button("Find Subclasses", variant="primary")
|
| 2036 |
-
with gr.Column():
|
| 2037 |
-
inherit_output = gr.Textbox(label="Inheriting Classes", lines=20, max_lines=30)
|
| 2038 |
-
inherit_btn.click(fn=find_classes_inheriting_from, inputs=base_class_input, outputs=inherit_output)
|
| 2039 |
-
gr.Markdown(_tool_doc_md(find_classes_inheriting_from))
|
| 2040 |
-
|
| 2041 |
with gr.Tab("📖 Context"):
|
| 2042 |
gr.Markdown("### Get Chunk Context")
|
| 2043 |
with gr.Row():
|
|
@@ -2080,11 +2165,12 @@ def create_gradio_app():
|
|
| 2080 |
dir_path = gr.Textbox(label="Directory Path (empty for root)", placeholder="e.g., src/")
|
| 2081 |
file_pattern = gr.Textbox(label="Pattern", value="*", placeholder="e.g., *.py")
|
| 2082 |
file_recursive = gr.Checkbox(label="Recursive", value=True)
|
| 2083 |
-
file_limit = gr.Slider(10, 100, value=50, step=10, label="
|
|
|
|
| 2084 |
list_files_btn = gr.Button("List Files", variant="primary")
|
| 2085 |
with gr.Column():
|
| 2086 |
list_files_output = gr.Textbox(label="Files", lines=20, max_lines=30)
|
| 2087 |
-
list_files_btn.click(fn=list_files_in_directory, inputs=[dir_path, file_pattern, file_recursive, file_limit], outputs=list_files_output)
|
| 2088 |
gr.Markdown(_tool_doc_md(list_files_in_directory))
|
| 2089 |
|
| 2090 |
gr.Markdown("---")
|
|
@@ -2092,11 +2178,12 @@ def create_gradio_app():
|
|
| 2092 |
with gr.Row():
|
| 2093 |
with gr.Column():
|
| 2094 |
import_module = gr.Textbox(label="Module/Entity Name", placeholder="e.g., torch, numpy...")
|
| 2095 |
-
import_limit = gr.Slider(10, 50, value=30, step=5, label="
|
|
|
|
| 2096 |
find_imports_btn = gr.Button("Find Files", variant="primary")
|
| 2097 |
with gr.Column():
|
| 2098 |
find_imports_output = gr.Textbox(label="Importing Files", lines=20, max_lines=30)
|
| 2099 |
-
find_imports_btn.click(fn=find_files_importing, inputs=[import_module, import_limit], outputs=find_imports_output)
|
| 2100 |
gr.Markdown(_tool_doc_md(find_files_importing))
|
| 2101 |
|
| 2102 |
gr.Markdown("---")
|
|
|
|
| 245 |
|
| 246 |
|
| 247 |
@observe(as_type="tool")
|
| 248 |
+
def search_nodes(query: str, limit: int = 10, page: int = 1) -> str:
|
| 249 |
"""
|
| 250 |
Search for chunk nodes in the knowledge graph by query string.
|
| 251 |
|
|
|
|
| 253 |
|
| 254 |
Args:
|
| 255 |
query: The search string to match against code index
|
| 256 |
+
limit: Maximum number of results to return per page (default: 10)
|
| 257 |
+
page: Page number for pagination, 1-indexed (default: 1)
|
| 258 |
|
| 259 |
Returns:
|
| 260 |
str: A formatted string with search results
|
|
|
|
| 270 |
except ValueError:
|
| 271 |
return f"Error: 'limit' must be an integer, got '{limit}'"
|
| 272 |
|
| 273 |
+
# Convert page to int if it's a string
|
| 274 |
+
if isinstance(page, str):
|
| 275 |
+
try:
|
| 276 |
+
page = int(page)
|
| 277 |
+
except ValueError:
|
| 278 |
+
return f"Error: 'page' must be an integer, got '{page}'"
|
| 279 |
+
|
| 280 |
if limit <= 0:
|
| 281 |
return "Error: limit must be a positive integer"
|
| 282 |
+
if page < 1:
|
| 283 |
+
return "Error: 'page' must be a positive integer (1 or greater)"
|
| 284 |
|
| 285 |
+
# Fetch more results to support pagination
|
| 286 |
+
max_fetch = limit * page
|
| 287 |
+
results = knowledge_graph.code_index.query(query, n_results=max_fetch)
|
| 288 |
metadatas = results.get("metadatas", [[]])[0]
|
| 289 |
|
| 290 |
if not metadatas:
|
| 291 |
return f"No results found for '{query}'."
|
| 292 |
|
| 293 |
+
total = len(metadatas)
|
| 294 |
+
# Pagination
|
| 295 |
+
total_pages = (total + limit - 1) // limit
|
| 296 |
+
if page > total_pages:
|
| 297 |
+
return f"Error: Page {page} does not exist. Total pages: {total_pages} (with {total} results at {limit} per page)"
|
| 298 |
+
|
| 299 |
+
start_idx = (page - 1) * limit
|
| 300 |
+
end_idx = start_idx + limit
|
| 301 |
+
page_slice = metadatas[start_idx:end_idx]
|
| 302 |
+
|
| 303 |
+
result = f"Search Results for '{query}' (Page {page}/{total_pages}, {total} total):\n"
|
| 304 |
result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
| 305 |
|
| 306 |
+
for i, res in enumerate(page_slice, start=start_idx + 1):
|
| 307 |
result += f"{i}. ID: {res.get('id', 'N/A')}\n"
|
| 308 |
content = res.get('content', '')
|
| 309 |
if content:
|
| 310 |
result += f" Content: {content}\n"
|
| 311 |
result += "\n"
|
| 312 |
|
| 313 |
+
# Pagination hint
|
| 314 |
+
if page < total_pages:
|
| 315 |
+
result += f"Use page={page + 1} to see the next page\n"
|
| 316 |
+
|
| 317 |
return result
|
| 318 |
except Exception as e:
|
| 319 |
return f"Error: {str(e)}"
|
| 320 |
|
|
|
|
| 321 |
@observe(as_type="tool")
|
| 322 |
def get_graph_stats() -> str:
|
| 323 |
"""
|
| 324 |
+
Get comprehensive statistics about the knowledge graph.
|
| 325 |
+
|
| 326 |
+
Returns detailed information about the repository structure including:
|
| 327 |
+
- Chunks: Code segments that represent portions of files (functions, classes, etc.)
|
| 328 |
+
- Entities: Programming constructs like classes, functions, methods, variables
|
| 329 |
+
- Files and directories in the repository
|
| 330 |
+
- Relationships between different components
|
| 331 |
|
| 332 |
+
For entity nodes, provides a breakdown by entity type (class, function, method, etc.).
|
| 333 |
|
| 334 |
Returns:
|
| 335 |
+
str: A formatted string with comprehensive graph statistics
|
| 336 |
"""
|
| 337 |
if knowledge_graph is None:
|
| 338 |
return "Error: Knowledge graph not initialized"
|
|
|
|
| 342 |
num_nodes = g.number_of_nodes()
|
| 343 |
num_edges = g.number_of_edges()
|
| 344 |
|
| 345 |
+
# Count node types
|
| 346 |
node_types = {}
|
| 347 |
+
entity_breakdown = {}
|
| 348 |
+
|
| 349 |
for _, node_attrs in g.nodes(data=True):
|
| 350 |
node_type = getattr(node_attrs['data'], 'node_type', 'Unknown')
|
| 351 |
node_types[node_type] = node_types.get(node_type, 0) + 1
|
| 352 |
+
|
| 353 |
+
# For entity nodes, get entity_type breakdown
|
| 354 |
+
if node_type == 'entity':
|
| 355 |
+
entity_type = getattr(node_attrs['data'], 'entity_type', 'Unknown')
|
| 356 |
+
|
| 357 |
+
# Fallback: if entity_type is empty, check entities dictionary
|
| 358 |
+
if not entity_type:
|
| 359 |
+
node_id = node_attrs['data'].id if hasattr(node_attrs['data'], 'id') else None
|
| 360 |
+
if node_id and node_id in knowledge_graph.entities:
|
| 361 |
+
entity_types = knowledge_graph.entities[node_id].get('type', [])
|
| 362 |
+
entity_type = entity_types[0] if entity_types else 'Unknown'
|
| 363 |
+
|
| 364 |
+
entity_breakdown[entity_type] = entity_breakdown.get(entity_type, 0) + 1
|
| 365 |
|
| 366 |
+
# Count edge relations
|
| 367 |
edge_relations = {}
|
| 368 |
for _, _, attrs in g.edges(data=True):
|
| 369 |
relation = attrs.get('relation', 'Unknown')
|
| 370 |
edge_relations[relation] = edge_relations.get(relation, 0) + 1
|
| 371 |
|
| 372 |
+
# Build result
|
| 373 |
result = f"""Knowledge Graph Statistics:
|
| 374 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 375 |
|
| 376 |
+
📊 Overview:
|
| 377 |
+
Total Nodes: {num_nodes:,}
|
| 378 |
+
Total Edges: {num_edges:,}
|
| 379 |
|
| 380 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 381 |
+
|
| 382 |
+
📦 Node Types:
|
| 383 |
"""
|
| 384 |
+
|
| 385 |
+
# Sort node types by count
|
| 386 |
for ntype, count in sorted(node_types.items(), key=lambda x: x[1], reverse=True):
|
| 387 |
+
result += f" • {ntype}: {count:,}\n"
|
| 388 |
+
|
| 389 |
+
# If this is entity type, show breakdown
|
| 390 |
+
if ntype == 'entity' and entity_breakdown:
|
| 391 |
+
result += f" └─ Entity Breakdown:\n"
|
| 392 |
+
for etype, ecount in sorted(entity_breakdown.items(), key=lambda x: x[1], reverse=True):
|
| 393 |
+
percentage = (ecount / count * 100) if count > 0 else 0
|
| 394 |
+
result += f" ├─ {etype}: {ecount:,} ({percentage:.1f}%)\n"
|
| 395 |
|
| 396 |
+
result += f"""
|
| 397 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 398 |
+
|
| 399 |
+
🔗 Edge Relations:
|
| 400 |
+
"""
|
| 401 |
for relation, count in sorted(edge_relations.items(), key=lambda x: x[1], reverse=True):
|
| 402 |
+
result += f" • {relation}: {count:,}\n"
|
| 403 |
+
|
| 404 |
+
# Add explanation section
|
| 405 |
+
result += f"""
|
| 406 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 407 |
+
|
| 408 |
+
ℹ️ Definitions:
|
| 409 |
+
|
| 410 |
+
Chunks: Code segments representing logical portions of files. Each chunk
|
| 411 |
+
contains a section of code (like a function, class, or code block)
|
| 412 |
+
along with metadata about what entities it declares and calls.
|
| 413 |
+
|
| 414 |
+
Entities: Programming constructs identified in the code including:
|
| 415 |
+
- Classes: Class definitions
|
| 416 |
+
- Functions: Function definitions
|
| 417 |
+
- Methods: Class method definitions
|
| 418 |
+
- Variables: Variable declarations
|
| 419 |
+
- Parameters: Function/method parameters
|
| 420 |
+
- Function_call/Method_call: Usage references
|
| 421 |
+
|
| 422 |
+
Files: Source code files in the repository
|
| 423 |
+
Directories: Folder structure containing files
|
| 424 |
+
Repo: Root repository node
|
| 425 |
+
|
| 426 |
+
Edge Relations:
|
| 427 |
+
- contains: Parent-child relationships (file contains chunks)
|
| 428 |
+
- declares: Entity declaration relationships
|
| 429 |
+
- calls: Entity usage/invocation relationships
|
| 430 |
+
"""
|
| 431 |
|
| 432 |
return result
|
| 433 |
except Exception as e:
|
|
|
|
| 634 |
|
| 635 |
|
| 636 |
@observe(as_type="tool")
|
| 637 |
+
def find_usages(entity_name: str, limit: int = 20, page: int = 1) -> str:
|
| 638 |
"""
|
| 639 |
Retrieve all usages or calls of an entity in the codebase.
|
| 640 |
|
|
|
|
| 642 |
|
| 643 |
Args:
|
| 644 |
entity_name: The name of the entity to retrieve usages for
|
| 645 |
+
limit: Maximum number of usages to return per page (default: 20)
|
| 646 |
+
page: Page number for pagination, 1-indexed (default: 1)
|
| 647 |
|
| 648 |
Returns:
|
| 649 |
str: A formatted string with usage locations
|
|
|
|
| 659 |
except ValueError:
|
| 660 |
return f"Error: 'limit' must be an integer, got '{limit}'"
|
| 661 |
|
| 662 |
+
# Convert page to int if it's a string
|
| 663 |
+
if isinstance(page, str):
|
| 664 |
+
try:
|
| 665 |
+
page = int(page)
|
| 666 |
+
except ValueError:
|
| 667 |
+
return f"Error: 'page' must be an integer, got '{page}'"
|
| 668 |
+
|
| 669 |
if entity_name not in knowledge_graph.entities:
|
| 670 |
return f"Error: Entity '{entity_name}' not found in knowledge graph"
|
| 671 |
|
| 672 |
if limit <= 0:
|
| 673 |
return "Error: limit must be a positive integer"
|
| 674 |
+
if page < 1:
|
| 675 |
+
return "Error: 'page' must be a positive integer (1 or greater)"
|
| 676 |
|
| 677 |
entity_info = knowledge_graph.entities[entity_name]
|
| 678 |
calling_chunks = entity_info.get('calling_chunk_ids', [])
|
|
|
|
| 680 |
if not calling_chunks:
|
| 681 |
return f"Entity '{entity_name}' found but no usages identified."
|
| 682 |
|
| 683 |
+
total = len(calling_chunks)
|
| 684 |
+
# Pagination
|
| 685 |
+
total_pages = (total + limit - 1) // limit
|
| 686 |
+
if page > total_pages:
|
| 687 |
+
return f"Error: Page {page} does not exist. Total pages: {total_pages} (with {total} usages at {limit} per page)"
|
| 688 |
+
|
| 689 |
+
start_idx = (page - 1) * limit
|
| 690 |
+
end_idx = start_idx + limit
|
| 691 |
+
page_slice = calling_chunks[start_idx:end_idx]
|
| 692 |
+
|
| 693 |
+
result = f"Usages of '{entity_name}' (Page {page}/{total_pages}, {total} total):\n"
|
| 694 |
result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
| 695 |
|
| 696 |
+
for i, chunk_id in enumerate(page_slice, start=start_idx + 1):
|
| 697 |
if chunk_id in knowledge_graph.graph:
|
| 698 |
chunk = knowledge_graph.graph.nodes[chunk_id]['data']
|
| 699 |
result += f"{i}. {chunk.path} (chunk {chunk.order_in_file})\n"
|
| 700 |
result += f" Content:\n{chunk.content}\n\n"
|
| 701 |
|
| 702 |
+
# Pagination hint
|
| 703 |
+
if page < total_pages:
|
| 704 |
+
result += f"Use page={page + 1} to see the next page\n"
|
| 705 |
|
| 706 |
return result
|
| 707 |
except Exception as e:
|
|
|
|
| 763 |
|
| 764 |
|
| 765 |
@observe(as_type="tool")
|
| 766 |
+
def get_related_chunks(chunk_id: str, relation_type: str = "calls", limit: int = 20, page: int = 1) -> str:
|
| 767 |
"""
|
| 768 |
Retrieve chunks related to a given chunk by a specific relationship.
|
| 769 |
|
|
|
|
| 772 |
Args:
|
| 773 |
chunk_id: The ID of the chunk to retrieve related chunks for
|
| 774 |
relation_type: The type of relationship to filter by (default: 'calls')
|
| 775 |
+
limit: Maximum number of results per page (default: 20)
|
| 776 |
+
page: Page number for pagination, 1-indexed (default: 1)
|
| 777 |
|
| 778 |
Returns:
|
| 779 |
str: A formatted string with related chunks
|
|
|
|
| 785 |
if chunk_id not in knowledge_graph.graph:
|
| 786 |
return f"Error: Chunk '{chunk_id}' not found in knowledge graph"
|
| 787 |
|
| 788 |
+
# Convert limit/page to int if they're strings
|
| 789 |
+
if isinstance(limit, str):
|
| 790 |
+
try:
|
| 791 |
+
limit = int(limit)
|
| 792 |
+
except ValueError:
|
| 793 |
+
return f"Error: 'limit' must be an integer, got '{limit}'"
|
| 794 |
+
|
| 795 |
+
if isinstance(page, str):
|
| 796 |
+
try:
|
| 797 |
+
page = int(page)
|
| 798 |
+
except ValueError:
|
| 799 |
+
return f"Error: 'page' must be an integer, got '{page}'"
|
| 800 |
+
|
| 801 |
+
if limit <= 0:
|
| 802 |
+
return "Error: limit must be a positive integer"
|
| 803 |
+
if page < 1:
|
| 804 |
+
return "Error: 'page' must be a positive integer (1 or greater)"
|
| 805 |
+
|
| 806 |
related = []
|
| 807 |
if relation_type == "" or relation_type == "all":
|
| 808 |
# Get all outgoing edges regardless of relation type
|
|
|
|
| 826 |
if not related:
|
| 827 |
return f"No chunks found with '{relation_type}' relationship from '{chunk_id}'"
|
| 828 |
|
| 829 |
+
total = len(related)
|
| 830 |
+
# Pagination
|
| 831 |
+
total_pages = (total + limit - 1) // limit
|
| 832 |
+
if page > total_pages:
|
| 833 |
+
return f"Error: Page {page} does not exist. Total pages: {total_pages} (with {total} results at {limit} per page)"
|
| 834 |
+
|
| 835 |
+
start_idx = (page - 1) * limit
|
| 836 |
+
end_idx = start_idx + limit
|
| 837 |
+
page_slice = related[start_idx:end_idx]
|
| 838 |
+
|
| 839 |
+
result = f"Chunks related to '{chunk_id}' via '{relation_type}' (Page {page}/{total_pages}, {total} total):\n"
|
| 840 |
result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
| 841 |
|
| 842 |
+
for i, chunk in enumerate(page_slice, start=start_idx + 1):
|
| 843 |
result += f"{i}. {chunk['id']}\n"
|
| 844 |
result += f" File: {chunk['file_path']}\n"
|
| 845 |
if chunk['entity_name']:
|
| 846 |
result += f" Entity: {chunk['entity_name']}\n"
|
| 847 |
result += "\n"
|
| 848 |
|
| 849 |
+
# Pagination hint
|
| 850 |
+
if page < total_pages:
|
| 851 |
+
result += f"Use page={page + 1} to see the next page\n"
|
| 852 |
|
| 853 |
return result
|
| 854 |
except Exception as e:
|
|
|
|
| 1202 |
except ValueError:
|
| 1203 |
return f"Error: 'page' must be an integer, got '{page}'"
|
| 1204 |
|
| 1205 |
+
# Convert partial_allowed to bool if it's a string
|
| 1206 |
+
if isinstance(partial_allowed, str):
|
| 1207 |
+
partial_allowed = partial_allowed.lower() in ('true', '1', 'yes')
|
| 1208 |
|
| 1209 |
if limit <= 0:
|
| 1210 |
return "Error: limit must be a positive integer"
|
|
|
|
| 1215 |
matches = []
|
| 1216 |
query_lower = name_query.lower()
|
| 1217 |
|
| 1218 |
+
# Build regex pattern for partial_allowed matching
|
| 1219 |
# This will match names containing all characters of the query in order
|
| 1220 |
+
if partial_allowed:
|
| 1221 |
# Create pattern that matches query as substring or with characters spread out
|
| 1222 |
# e.g., "Embed" matches "Embedding", "BertEmbeddings", "EmbedLayer"
|
| 1223 |
+
partial_pattern = '.*'.join(re.escape(c) for c in query_lower)
|
| 1224 |
+
partial_regex = re.compile(partial_pattern, re.IGNORECASE)
|
| 1225 |
|
| 1226 |
for nid, n in g.nodes(data=True):
|
| 1227 |
node = n['data']
|
|
|
|
| 1232 |
|
| 1233 |
# Check if name matches the query
|
| 1234 |
name_matches = False
|
| 1235 |
+
if partial_allowed:
|
| 1236 |
+
# Partial match: substring match OR regex pattern match
|
| 1237 |
+
if query_lower in node_name.lower() or partial_regex.search(node_name):
|
| 1238 |
name_matches = True
|
| 1239 |
else:
|
| 1240 |
# Exact substring match
|
|
|
|
| 1516 |
|
| 1517 |
|
| 1518 |
@observe(as_type="tool")
|
| 1519 |
+
def list_files_in_directory(directory_path: str = "", pattern: str = "*", recursive: bool = True, limit: int = 50, page: int = 1) -> str:
|
| 1520 |
"""
|
| 1521 |
List files in a directory with optional glob pattern matching.
|
| 1522 |
|
|
|
|
| 1527 |
directory_path: Path to the directory to list (empty string for root/all files)
|
| 1528 |
pattern: Glob pattern to filter files (e.g., '*.py', 'test_*.py', '**/*.js')
|
| 1529 |
recursive: Whether to search recursively in subdirectories (default: True)
|
| 1530 |
+
limit: Maximum number of files to return per page (default: 50)
|
| 1531 |
+
page: Page number for pagination, 1-indexed (default: 1)
|
| 1532 |
|
| 1533 |
Returns:
|
| 1534 |
str: A formatted string with matching files
|
|
|
|
| 1544 |
except ValueError:
|
| 1545 |
return f"Error: 'limit' must be an integer, got '{limit}'"
|
| 1546 |
|
| 1547 |
+
# Convert page to int if it's a string
|
| 1548 |
+
if isinstance(page, str):
|
| 1549 |
+
try:
|
| 1550 |
+
page = int(page)
|
| 1551 |
+
except ValueError:
|
| 1552 |
+
return f"Error: 'page' must be an integer, got '{page}'"
|
| 1553 |
+
|
| 1554 |
+
if limit <= 0:
|
| 1555 |
+
return "Error: limit must be a positive integer"
|
| 1556 |
+
if page < 1:
|
| 1557 |
+
return "Error: 'page' must be a positive integer (1 or greater)"
|
| 1558 |
+
|
| 1559 |
# Convert recursive to bool if it's a string
|
| 1560 |
if isinstance(recursive, str):
|
| 1561 |
recursive = recursive.lower() in ('true', '1', 'yes')
|
|
|
|
| 1603 |
'language': language,
|
| 1604 |
'entity_count': len(declared_entities)
|
| 1605 |
})
|
|
|
|
|
|
|
|
|
|
| 1606 |
|
| 1607 |
# Sort by path for consistent ordering
|
| 1608 |
matching_files.sort(key=lambda x: x['path'])
|
|
|
|
| 1612 |
pattern_desc = f" matching '{pattern}'" if pattern and pattern != '*' else ""
|
| 1613 |
return f"No files found{filter_desc}{pattern_desc}."
|
| 1614 |
|
| 1615 |
+
total = len(matching_files)
|
| 1616 |
+
# Pagination
|
| 1617 |
+
total_pages = (total + limit - 1) // limit
|
| 1618 |
+
if page > total_pages:
|
| 1619 |
+
return f"Error: Page {page} does not exist. Total pages: {total_pages} (with {total} files at {limit} per page)"
|
| 1620 |
+
|
| 1621 |
+
start_idx = (page - 1) * limit
|
| 1622 |
+
end_idx = start_idx + limit
|
| 1623 |
+
page_slice = matching_files[start_idx:end_idx]
|
| 1624 |
+
|
| 1625 |
result = f"Files"
|
| 1626 |
if directory_path:
|
| 1627 |
result += f" in '{directory_path}'"
|
| 1628 |
if pattern and pattern != '*':
|
| 1629 |
result += f" matching '{pattern}'"
|
| 1630 |
+
result += f" (Page {page}/{total_pages}, {total} total):\n"
|
| 1631 |
result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
| 1632 |
|
| 1633 |
+
for i, f in enumerate(page_slice, start=start_idx + 1):
|
| 1634 |
result += f"{i}. {f['path']}\n"
|
| 1635 |
result += f" Language: {f['language']}, Entities: {f['entity_count']}\n\n"
|
| 1636 |
|
| 1637 |
+
# Pagination hint
|
| 1638 |
+
if page < total_pages:
|
| 1639 |
+
result += f"Use page={page + 1} to see the next page\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1640 |
|
| 1641 |
return result
|
| 1642 |
except Exception as e:
|
|
|
|
| 1644 |
|
| 1645 |
|
| 1646 |
@observe(as_type="tool")
|
| 1647 |
+
def find_files_importing(module_or_entity: str, limit: int = 30, page: int = 1) -> str:
|
| 1648 |
"""
|
| 1649 |
Retrieve all files that import a specific module or entity.
|
| 1650 |
|
|
|
|
| 1652 |
|
| 1653 |
Args:
|
| 1654 |
module_or_entity: The name of the module or entity to retrieve imports of
|
| 1655 |
+
limit: Maximum number of results to return per page (default: 30)
|
| 1656 |
+
page: Page number for pagination, 1-indexed (default: 1)
|
| 1657 |
|
| 1658 |
Returns:
|
| 1659 |
str: A formatted string with files that import the specified module/entity
|
|
|
|
| 1668 |
limit = int(limit)
|
| 1669 |
except ValueError:
|
| 1670 |
return f"Error: 'limit' must be an integer, got '{limit}'"
|
| 1671 |
+
|
| 1672 |
+
# Convert page to int if it's a string
|
| 1673 |
+
if isinstance(page, str):
|
| 1674 |
+
try:
|
| 1675 |
+
page = int(page)
|
| 1676 |
+
except ValueError:
|
| 1677 |
+
return f"Error: 'page' must be an integer, got '{page}'"
|
| 1678 |
+
|
| 1679 |
+
if limit <= 0:
|
| 1680 |
+
return "Error: limit must be a positive integer"
|
| 1681 |
+
if page < 1:
|
| 1682 |
+
return "Error: 'page' must be a positive integer (1 or greater)"
|
| 1683 |
|
| 1684 |
g = knowledge_graph.graph
|
| 1685 |
importing_files = []
|
|
|
|
| 1735 |
'match_type': 'import_statement'
|
| 1736 |
})
|
| 1737 |
break
|
|
|
|
|
|
|
|
|
|
| 1738 |
|
| 1739 |
# Sort by path
|
| 1740 |
importing_files.sort(key=lambda x: x['path'])
|
|
|
|
| 1742 |
if not importing_files:
|
| 1743 |
return f"No files found importing '{module_or_entity}'.\n\nTip: Try searching for the module name in code content using search_nodes."
|
| 1744 |
|
| 1745 |
+
total = len(importing_files)
|
| 1746 |
+
# Pagination
|
| 1747 |
+
total_pages = (total + limit - 1) // limit
|
| 1748 |
+
if page > total_pages:
|
| 1749 |
+
return f"Error: Page {page} does not exist. Total pages: {total_pages} (with {total} files at {limit} per page)"
|
| 1750 |
+
|
| 1751 |
+
start_idx = (page - 1) * limit
|
| 1752 |
+
end_idx = start_idx + limit
|
| 1753 |
+
page_slice = importing_files[start_idx:end_idx]
|
| 1754 |
+
|
| 1755 |
+
result = f"Files importing '{module_or_entity}' (Page {page}/{total_pages}, {total} total):\n"
|
| 1756 |
result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
|
| 1757 |
|
| 1758 |
+
for i, f in enumerate(page_slice, start=start_idx + 1):
|
| 1759 |
result += f"{i}. {f['path']}\n"
|
| 1760 |
result += f" Match type: {f['match_type']}\n"
|
| 1761 |
if f['matched_entities']:
|
| 1762 |
result += f" Matched: {', '.join(f['matched_entities'][:3])}\n"
|
| 1763 |
result += "\n"
|
| 1764 |
|
| 1765 |
+
# Pagination hint
|
| 1766 |
+
if page < total_pages:
|
| 1767 |
+
result += f"Use page={page + 1} to see the next page\n"
|
| 1768 |
+
|
| 1769 |
return result
|
| 1770 |
except Exception as e:
|
| 1771 |
return f"Error: {str(e)}"
|
|
|
|
| 1940 |
with gr.Row():
|
| 1941 |
with gr.Column():
|
| 1942 |
search_query = gr.Textbox(label="Search Query", placeholder="Enter search query...")
|
| 1943 |
+
search_limit = gr.Slider(1, 50, value=10, step=1, label="Results per Page")
|
| 1944 |
+
search_page = gr.Slider(1, 100, value=1, step=1, label="Page")
|
| 1945 |
search_btn = gr.Button("Search", variant="primary")
|
| 1946 |
with gr.Column():
|
| 1947 |
search_output = gr.Textbox(label="Search Results", lines=20, max_lines=30)
|
| 1948 |
+
search_btn.click(fn=search_nodes, inputs=[search_query, search_limit, search_page], outputs=search_output)
|
| 1949 |
gr.Markdown(_tool_doc_md(search_nodes))
|
| 1950 |
|
| 1951 |
with gr.Tab("📝 Node Info"):
|
|
|
|
| 2030 |
with gr.Row():
|
| 2031 |
with gr.Column():
|
| 2032 |
entity_name_usage = gr.Textbox(label="Entity Name", placeholder="Enter entity name...")
|
| 2033 |
+
usage_limit = gr.Slider(1, 50, value=20, step=1, label="Results per Page")
|
| 2034 |
+
usage_page = gr.Slider(1, 100, value=1, step=1, label="Page")
|
| 2035 |
usage_btn = gr.Button("Find Usages", variant="primary")
|
| 2036 |
with gr.Column():
|
| 2037 |
usage_output = gr.Textbox(label="Usages", lines=15, max_lines=25)
|
| 2038 |
+
usage_btn.click(fn=find_usages, inputs=[entity_name_usage, usage_limit, usage_page], outputs=usage_output)
|
| 2039 |
gr.Markdown(_tool_doc_md(find_usages))
|
| 2040 |
|
| 2041 |
with gr.Tab("🔬 Discovery"):
|
|
|
|
| 2065 |
search_name = gr.Textbox(label="Name Contains", placeholder="Enter partial name...")
|
| 2066 |
search_limit = gr.Slider(1, 100, value=10, step=1, label="Max Results")
|
| 2067 |
search_page = gr.Slider(1, 100, value=1, step=1, label="Page")
|
| 2068 |
+
search_partial_allowed = gr.Checkbox(label="Partial Match", value=True)
|
| 2069 |
search_type_btn = gr.Button("Search", variant="primary")
|
| 2070 |
with gr.Column():
|
| 2071 |
search_type_output = gr.Textbox(label="Results", lines=20, max_lines=30)
|
| 2072 |
+
search_type_btn.click(fn=search_by_type_and_name, inputs=[search_type, search_name, search_limit, search_page, search_partial_allowed], outputs=search_type_output)
|
| 2073 |
gr.Markdown(_tool_doc_md(search_by_type_and_name))
|
| 2074 |
|
| 2075 |
with gr.Tab("🔗 Relationships"):
|
|
|
|
| 2102 |
with gr.Column():
|
| 2103 |
related_chunk_id = gr.Textbox(label="Chunk ID", placeholder="Enter chunk ID...")
|
| 2104 |
relation_type = gr.Dropdown(choices=["" , "calls", "contains", "declares", "uses"], label="Relation Type", value="calls")
|
| 2105 |
+
related_limit = gr.Slider(1, 100, value=20, step=1, label="Results per Page")
|
| 2106 |
+
related_page = gr.Slider(1, 100, value=1, step=1, label="Page")
|
| 2107 |
related_btn = gr.Button("Get Related Chunks", variant="primary")
|
| 2108 |
with gr.Column():
|
| 2109 |
related_output = gr.Textbox(label="Related Chunks", lines=20, max_lines=30)
|
| 2110 |
+
related_btn.click(fn=get_related_chunks, inputs=[related_chunk_id, relation_type, related_limit, related_page], outputs=related_output)
|
| 2111 |
gr.Markdown(_tool_doc_md(get_related_chunks))
|
| 2112 |
|
| 2113 |
gr.Markdown("---")
|
|
|
|
| 2123 |
path_btn.click(fn=find_path, inputs=[path_source, path_target, path_depth], outputs=path_output)
|
| 2124 |
gr.Markdown(_tool_doc_md(find_path))
|
| 2125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2126 |
with gr.Tab("📖 Context"):
|
| 2127 |
gr.Markdown("### Get Chunk Context")
|
| 2128 |
with gr.Row():
|
|
|
|
| 2165 |
dir_path = gr.Textbox(label="Directory Path (empty for root)", placeholder="e.g., src/")
|
| 2166 |
file_pattern = gr.Textbox(label="Pattern", value="*", placeholder="e.g., *.py")
|
| 2167 |
file_recursive = gr.Checkbox(label="Recursive", value=True)
|
| 2168 |
+
file_limit = gr.Slider(10, 100, value=50, step=10, label="Results per Page")
|
| 2169 |
+
file_page = gr.Slider(1, 100, value=1, step=1, label="Page")
|
| 2170 |
list_files_btn = gr.Button("List Files", variant="primary")
|
| 2171 |
with gr.Column():
|
| 2172 |
list_files_output = gr.Textbox(label="Files", lines=20, max_lines=30)
|
| 2173 |
+
list_files_btn.click(fn=list_files_in_directory, inputs=[dir_path, file_pattern, file_recursive, file_limit, file_page], outputs=list_files_output)
|
| 2174 |
gr.Markdown(_tool_doc_md(list_files_in_directory))
|
| 2175 |
|
| 2176 |
gr.Markdown("---")
|
|
|
|
| 2178 |
with gr.Row():
|
| 2179 |
with gr.Column():
|
| 2180 |
import_module = gr.Textbox(label="Module/Entity Name", placeholder="e.g., torch, numpy...")
|
| 2181 |
+
import_limit = gr.Slider(10, 50, value=30, step=5, label="Results per Page")
|
| 2182 |
+
import_page = gr.Slider(1, 100, value=1, step=1, label="Page")
|
| 2183 |
find_imports_btn = gr.Button("Find Files", variant="primary")
|
| 2184 |
with gr.Column():
|
| 2185 |
find_imports_output = gr.Textbox(label="Importing Files", lines=20, max_lines=30)
|
| 2186 |
+
find_imports_btn.click(fn=find_files_importing, inputs=[import_module, import_limit, import_page], outputs=find_imports_output)
|
| 2187 |
gr.Markdown(_tool_doc_md(find_files_importing))
|
| 2188 |
|
| 2189 |
gr.Markdown("---")
|