lailaelkoussy commited on
Commit
547246f
·
1 Parent(s): a0b7cd2

add pagination + better graph stats

Browse files
Files changed (1) hide show
  1. 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 = knowledge_graph.code_index.query(query, n_results=limit)
 
 
276
  metadatas = results.get("metadatas", [[]])[0]
277
 
278
  if not metadatas:
279
  return f"No results found for '{query}'."
280
 
281
- result = f"Search Results for '{query}' ({len(metadatas)} results):\n"
 
 
 
 
 
 
 
 
 
 
282
  result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
283
 
284
- for i, res in enumerate(metadatas, 1):
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 overall statistics about the knowledge graph.
 
 
 
 
 
 
300
 
301
- Includes node and edge counts, types, and relations.
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
- Total Nodes: {num_nodes}
328
- Total Edges: {num_edges}
 
329
 
330
- Node Types:
 
 
331
  """
 
 
332
  for ntype, count in sorted(node_types.items(), key=lambda x: x[1], reverse=True):
333
- result += f" - {ntype}: {count}\n"
 
 
 
 
 
 
 
334
 
335
- result += "\nEdge Relations:\n"
 
 
 
 
336
  for relation, count in sorted(edge_relations.items(), key=lambda x: x[1], reverse=True):
337
- result += f" - {relation}: {count}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- result = f"Usages of '{entity_name}' ({len(calling_chunks)} total):\n"
 
 
 
 
 
 
 
 
 
 
581
  result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
582
 
583
- for i, chunk_id in enumerate(calling_chunks[:limit], 1):
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
- if len(calling_chunks) > limit:
590
- result += f"... and {len(calling_chunks) - limit} more usages\n"
 
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
- result = f"Chunks related to '{chunk_id}' via '{relation_type}' ({len(related)} total):\n"
 
 
 
 
 
 
 
 
 
 
696
  result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
697
 
698
- for i, chunk in enumerate(related[:15], 1):
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
- if len(related) > 15:
706
- result += f"... and {len(related) - 15} more\n"
 
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 fuzzy to bool if it's a string
1061
- if isinstance(fuzzy, str):
1062
- fuzzy = fuzzy.lower() in ('true', '1', 'yes')
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 fuzzy matching
1074
  # This will match names containing all characters of the query in order
1075
- if fuzzy:
1076
  # Create pattern that matches query as substring or with characters spread out
1077
  # e.g., "Embed" matches "Embedding", "BertEmbeddings", "EmbedLayer"
1078
- fuzzy_pattern = '.*'.join(re.escape(c) for c in query_lower)
1079
- fuzzy_regex = re.compile(fuzzy_pattern, re.IGNORECASE)
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 fuzzy:
1091
- # Fuzzy match: substring match OR regex pattern match
1092
- if query_lower in node_name.lower() or fuzzy_regex.search(node_name):
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" ({len(matching_files)} results):\n"
1466
  result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
1467
 
1468
- for i, f in enumerate(matching_files, 1):
1469
  result += f"{i}. {f['path']}\n"
1470
  result += f" Language: {f['language']}, Entities: {f['entity_count']}\n\n"
1471
 
1472
- return result
1473
- except Exception as e:
1474
- return f"Error: {str(e)}"
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
- result = f"Files importing '{module_or_entity}' ({len(importing_files)} results):\n"
 
 
 
 
 
 
 
 
 
 
1668
  result += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
1669
 
1670
- for i, f in enumerate(importing_files, 1):
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="Max Results")
 
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="Max Results")
 
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
- search_fuzzy = gr.Checkbox(label="Fuzzy Match", value=True)
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, search_fuzzy], outputs=search_type_output)
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="Max Results")
 
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="Max Results")
 
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("---")