fix graphs

This commit is contained in:
2025-11-14 00:07:30 +00:00
parent 163219f762
commit 6a1d5438df
+157 -70
View File
@@ -41,7 +41,9 @@ OLLAMA_BASE_URL = "http://127.0.0.1:11434"
os.environ["OLLAMA_HOST"] = OLLAMA_BASE_URL os.environ["OLLAMA_HOST"] = OLLAMA_BASE_URL
os.environ["OLLAMA_API_BASE"] = OLLAMA_BASE_URL os.environ["OLLAMA_API_BASE"] = OLLAMA_BASE_URL
CODEBASE_PATH = Path(os.environ.get("CODEBASE_PATH", ".")) # change as needed CODEBASE_PATH = Path("./working_repo")
CODEBASE_PATH.mkdir(exist_ok=True)
VECTOR_DB_PATH = Path(os.environ.get("VECTOR_DB_PATH", "./chroma_db")) VECTOR_DB_PATH = Path(os.environ.get("VECTOR_DB_PATH", "./chroma_db"))
BM25_INDEX_PATH = Path(os.environ.get("BM25_INDEX_PATH", "./bm25_index.json")) BM25_INDEX_PATH = Path(os.environ.get("BM25_INDEX_PATH", "./bm25_index.json"))
RAGIGNORE_PATH = CODEBASE_PATH / ".ragignore" RAGIGNORE_PATH = CODEBASE_PATH / ".ragignore"
@@ -3006,28 +3008,45 @@ def find_brace_block_end(lines: List[str], start_idx: int) -> Optional[int]:
# ----------------------------- # -----------------------------
@mcp.tool() @mcp.tool()
def health_check() -> str: def health_check() -> str:
"""What it does - Quick JSON status of the RAG server (ready, indexed path, chunk count, age). """What it does - Quick JSON status of the RAG server (ready, indexed path, chunk count, age, graph stats).
When to use - Before any other query, to confirm the index is current. When to use - Before any other query, to confirm the index is current and see codebase structure.
When not to use - After you already know the server is healthy; it adds no value. When not to use - After you already know the server is healthy; it adds no value.
Example - health_check() → { "status":"ready","total_chunks":11234,"index_age_hours":4.7 }""" Example - health_check() → { "status":"ready","total_chunks":11234,"graph_nodes":4567,"graph_edges":12345,"index_age_hours":4.7 }"""
try: try:
with _startup_lock: with _startup_lock:
# Check if we have a git repo in working_repo
repo_info = "No repository loaded"
if (CODEBASE_PATH / ".git").exists():
try:
branch = subprocess.check_output(
["git", "-C", str(CODEBASE_PATH), "rev-parse", "--abbrev-ref", "HEAD"],
text=True
).strip()
repo_info = f"Loaded: {branch}"
except:
repo_info = "Git repository (no branch info)"
status = { status = {
"status": "ready" if vectorstore is not None else "initializing", "status": "ready" if vectorstore is not None else "no_index",
"codebase": str(CODEBASE_PATH), "working_repository": str(CODEBASE_PATH),
"repo_status": repo_info,
"ollama_url": OLLAMA_BASE_URL, "ollama_url": OLLAMA_BASE_URL,
"config": { "config": {
"embedding_model": EMBEDDING_MODEL, "embedding_model": EMBEDDING_MODEL,
"vector_weight": VECTOR_WEIGHT, "vector_weight": VECTOR_WEIGHT,
"bm25_weight": BM25_WEIGHT, "bm25_weight": BM25_WEIGHT,
"rerank_enabled": ENABLE_RERANK, "rerank_enabled": ENABLE_RERANK,
"batch_size": EMBEDDING_BATCH_SIZE "batch_size": EMBEDDING_BATCH_SIZE,
"graph_enabled": True
}, },
"Tools": { "Tools": {
"NOTE": "THESE TOOLS ARE RESTRICTED BY .gitignore AS WELL AS .ragignore", "NOTE": "THESE TOOLS ARE RESTRICTED BY .gitignore AS WELL AS .ragignore",
"search_codebase": search_codebase.__doc__, "search_codebase": "Hybrid RAG search with graph context (cross-file deps, inheritance, calls)",
"find_code_references": find_code_references.__doc__, "find_code_references": "Find exact symbol references across codebase",
"read_file_lines_tool": read_file_lines_tool.__doc__ "read_file_lines_tool": "Read specific file lines with syntax highlighting",
"find_path": "Find files by glob patterns",
"grep": "Search file contents with regex",
"list_directory": "List directory contents"
} }
} }
@@ -3040,10 +3059,81 @@ Example - health_check() → { "status":"ready","total_chunks":11234,"index_age_
"index_age_hours": round((time.time() - index_build_time) / 3600, 1) if index_build_time > 0 else None "index_age_hours": round((time.time() - index_build_time) / 3600, 1) if index_build_time > 0 else None
} }
# Add graph statistics
if graph_loaded:
graph_stats = _calculate_graph_stats(graph)
status["graph"] = graph_stats
else:
status["graph"] = {
"status": "not_loaded",
"nodes": 0,
"edges": 0,
"message": "Graph will be built on next index rebuild"
}
return json.dumps(status, indent=2) return json.dumps(status, indent=2)
except Exception as e: except Exception as e:
return json.dumps({"status": "error", "message": str(e)}, indent=2) return json.dumps({"status": "error", "message": str(e)}, indent=2)
def _calculate_graph_stats(graph: LocalGraph) -> dict:
"""Calculate comprehensive graph statistics."""
if graph.graph.number_of_nodes() == 0:
return {"status": "empty", "nodes": 0, "edges": 0}
# Basic counts
nodes = graph.graph.number_of_nodes()
edges = graph.graph.number_of_edges()
# Count by node type
node_types = {}
for _, data in graph.graph.nodes(data=True):
node_type = data.get('type', 'unknown')
node_types[node_type] = node_types.get(node_type, 0) + 1
# Count by edge type
edge_types = {}
for _, _, data in graph.graph.edges(data=True):
edge_type = data.get('type', 'unknown')
edge_types[edge_type] = edge_types.get(edge_type, 0) + 1
# Language distribution
languages = {}
for _, data in graph.graph.nodes(data=True):
lang = data.get('language', 'unknown')
languages[lang] = languages.get(lang, 0) + 1
# File statistics
files = [n for n, d in graph.graph.nodes(data=True) if d.get('type') == 'File']
# Relationship density
avg_edges_per_node = edges / nodes if nodes > 0 else 0
# Most connected nodes (hubs)
degree_centrality = dict(graph.graph.degree())
top_hubs = sorted(degree_centrality.items(), key=lambda x: x[1], reverse=True)[:5]
top_hubs_info = []
for node_id, degree in top_hubs:
node_data = graph.graph.nodes[node_id]
top_hubs_info.append({
"name": node_data.get('name', node_id),
"type": node_data.get('type', 'unknown'),
"file": node_data.get('file', ''),
"connections": degree
})
return {
"status": "loaded",
"nodes": nodes,
"edges": edges,
"node_types": node_types,
"edge_types": edge_types,
"languages": languages,
"files": len(files),
"avg_edges_per_node": round(avg_edges_per_node, 2),
"top_connected_nodes": top_hubs_info,
"relationship_density": "high" if avg_edges_per_node > 2.0 else "medium" if avg_edges_per_node > 1.0 else "low"
}
@mcp.tool() @mcp.tool()
def search_codebase(query: str, top_k: int = 5, rerank: bool = True) -> str: def search_codebase(query: str, top_k: int = 5, rerank: bool = True) -> str:
"""Hybrid RAG search: returns ranked code snippets + file/line + docstrings + graph context (cross-file deps, inheritance, callers/callees, type refs). """Hybrid RAG search: returns ranked code snippets + file/line + docstrings + graph context (cross-file deps, inheritance, callers/callees, type refs).
@@ -3056,8 +3146,14 @@ rerank=False: fast, 65% accuracy | rerank=True: slower, 95% accuracy
Output includes: code content, docs, cross_file:[calls/extends/uses@filepath], used_by:[entity@filepath], file:[same-file entities] Output includes: code content, docs, cross_file:[calls/extends/uses@filepath], used_by:[entity@filepath], file:[same-file entities]
Example: search_codebase("user authentication", 5, True) → 5 results with code + "cross_file:calls:ValidateToken@auth/jwt.go|used_by:setupAuth@server/routes.go" """ Example: search_codebase("user authentication", 5, True) → 5 results with code + "cross_file:calls:ValidateToken@auth/jwt.go|used_by:setupAuth@server/routes.go" """
if vectorstore is None or bm25_corpus is None:
return "❌ Index not built. Please call init_repo() or rebuild_index() first."
graph = LocalGraph(CODEBASE_PATH) graph = LocalGraph(CODEBASE_PATH)
graph.load() graph_loaded = graph.load()
if not graph_loaded:
logger.warning("Graph not loaded - search will proceed without graph context")
try: try:
hy = hybrid_search(query, k=max(top_k, RERANK_TOP_N)) hy = hybrid_search(query, k=max(top_k, RERANK_TOP_N))
@@ -3396,61 +3492,42 @@ Note: This tool treats / as '/home/popertots/Crussell/' so adjust paths accordin
@mcp.tool() @mcp.tool()
def init_repo(git_url: str) -> str: def init_repo(git_url: str) -> str:
"""What it does - Initialise the RAG stack with a brand-new Git repository or update an existing one. """What it does - Initialise the RAG stack with a brand-new Git repository."""
The function will:
1. Clone the repo if it does not already exist locally,
2. Pull the latest changes if it does,
3. Reset the global `CODEBASE_PATH` to the local clone,
4. Remove any stale index artefacts,
5. Re-build the vector/BM25 indexes with the same lock-protected flow used by `rebuild_index`,
6. Return a human-readable status message.
Next steps:
• Call `health_check` to confirm that the server is ready.
• Use `search_codebase` or `find_code_references` to explore the new code.
• Use `read_file_lines_tool` to inspect any hit in detail.
"""
import shutil import shutil
from pathlib import Path from pathlib import Path
import subprocess import subprocess
# ----- 1. Determine where to clone --------------------------------- # ----- 1. Always wipe and recreate working_repo ---------------------
repo_name = git_url.split('/')[-1] if CODEBASE_PATH.exists():
if repo_name.endswith('.git'): shutil.rmtree(CODEBASE_PATH)
repo_name = repo_name[:-4] logger.info(f"Wiped existing working repo: {CODEBASE_PATH}")
clone_dir = Path.cwd() / repo_name
# ----- 2. Clone / pull ----------------------------------------------- CODEBASE_PATH.mkdir(exist_ok=True)
# ----- 2. Clone fresh -----------------------------------------------
try: try:
if clone_dir.exists(): subprocess.run(
# Existing repo → pull ["git", "clone", git_url, str(CODEBASE_PATH)],
subprocess.run( check=True,
["git", "-C", str(clone_dir), "pull"], stdout=subprocess.PIPE,
check=True, stderr=subprocess.PIPE,
stdout=subprocess.PIPE, text=True,
stderr=subprocess.PIPE, )
text=True, logger.info(f"Cloned {git_url} to {CODEBASE_PATH}")
)
else:
# New repo → clone
subprocess.run(
["git", "clone", git_url],
cwd=str(Path.cwd()),
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
return f"❌ Failed to clone/pull {git_url}: {e.stderr.strip()}" return f"❌ Failed to clone {git_url}: {e.stderr.strip()}"
# ----- 3. Update the global CODEBASE_PATH -------------------------------- # ----- 3. Reset ALL global state ------------------------------------
global CODEBASE_PATH global vectorstore, bm25, bm25_corpus, chunks_metadata, index_build_time
CODEBASE_PATH = clone_dir
# ----- 4. Remove stale indices ---------------------------------------- vectorstore = None
for artefact in ("chroma_db", "bm25_index.json", "embeddings"): bm25 = None
bm25_corpus = None
chunks_metadata = None
index_build_time = 0
# ----- 4. Remove any stale indices ----------------------------------
for artefact in ("chroma_db", "bm25_index.json", "embeddings", ".mcp_cache"):
artefact_path = CODEBASE_PATH / artefact artefact_path = CODEBASE_PATH / artefact
if artefact_path.exists(): if artefact_path.exists():
try: try:
@@ -3458,27 +3535,37 @@ def init_repo(git_url: str) -> str:
shutil.rmtree(artefact_path) shutil.rmtree(artefact_path)
else: else:
artefact_path.unlink() artefact_path.unlink()
logger.info(f"Removed stale artefact: {artefact_path}")
except Exception as e: except Exception as e:
logger.warning(f"Could not delete {artefact_path}: {e}") logger.warning(f"Could not delete {artefact_path}: {e}")
# ----- 5. Re-build the index (protected by the startup lock) ------------ # ----- 5. Re-build the index ----------------------------------------
try: try:
rebuild_index() build_indexes()
logger.info("Index rebuilt successfully")
except Exception as e: except Exception as e:
logger.exception("Index rebuild failed")
return f"❌ Index rebuild failed: {e}" return f"❌ Index rebuild failed: {e}"
# ----- 6. Report success ----------------------------------------------- # ----- 6. Report success --------------------------------------------
branch_name = subprocess.check_output( try:
["git", "-C", str(CODEBASE_PATH), "rev-parse", "--abbrev-ref", "HEAD"], branch_name = subprocess.check_output(
text=True, ["git", "-C", str(CODEBASE_PATH), "rev-parse", "--abbrev-ref", "HEAD"],
).strip() text=True,
git_hash = subprocess.check_output( ).strip()
["git", "-C", str(CODEBASE_PATH), "rev-parse", "HEAD"], git_hash = subprocess.check_output(
text=True, ["git", "-C", str(CODEBASE_PATH), "rev-parse", "HEAD"],
).strip() text=True,
).strip()
return f"✅ Initialized repository at {CODEBASE_PATH} on branch {branch_name} with hash {git_hash}" # Verify the index was actually built
if vectorstore is None or bm25_corpus is None:
return f"⚠️ Repository cloned but index failed to build properly"
return f"✅ Initialized repository at {CODEBASE_PATH}\n Branch: {branch_name}\n Commit: {git_hash[:8]}\n Indexed chunks: {len(bm25_corpus)}"
except Exception as e:
return f"⚠️ Repository cloned but status check failed: {e}"
@mcp.tool() @mcp.tool()
def rebuild_index() -> str: def rebuild_index() -> str: