Files
tirth8205--code-review-graph/code_review_graph/tools/context.py
T
2026-07-13 12:42:18 +08:00

153 lines
5.7 KiB
Python

"""Tool: get_minimal_context — ultra-compact context for token-efficient workflows."""
from __future__ import annotations
import logging
import sqlite3
import subprocess
from pathlib import Path
from typing import Any
from ._common import _get_store, compact_response
logger = logging.getLogger(__name__)
def _has_git_changes(root: Path, base: str) -> bool:
"""Quick check for uncommitted or diffed changes."""
try:
result = subprocess.run(
["git", "diff", "--name-only", base, "--"],
capture_output=True, stdin=subprocess.DEVNULL, text=True,
cwd=str(root), timeout=10,
)
if result.returncode == 0 and result.stdout.strip():
return True
# Also check staged/unstaged
result2 = subprocess.run(
["git", "status", "--porcelain"],
capture_output=True, stdin=subprocess.DEVNULL, text=True,
cwd=str(root), timeout=10,
)
return bool(result2.stdout.strip())
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
def get_minimal_context(
task: str = "",
changed_files: list[str] | None = None,
repo_root: str | None = None,
base: str = "HEAD~1",
) -> dict[str, Any]:
"""Return minimum context an agent needs to start any task (~100 tokens).
Combines graph stats, top communities, top flows, risk score,
and suggested next tools into an ultra-compact response.
Args:
task: Natural language description of what the agent is doing
(e.g. "review PR #42", "debug login timeout").
changed_files: Explicit changed files. Auto-detected from git if None.
repo_root: Repository root path. Auto-detected if None.
base: Git ref for diff comparison.
"""
store, root = _get_store(repo_root)
try:
# 1. Quick stats
stats = store.get_stats()
# 2. Risk from changed files
risk = "unknown"
risk_score = 0.0
top_affected: list[str] = []
test_gap_count = 0
if changed_files or _has_git_changes(root, base):
try:
from ..changes import analyze_changes
from ..incremental import get_changed_files as _get_changed
files = changed_files
if not files:
files = _get_changed(root, base)
if files:
abs_files = [str(root / f) for f in files]
analysis = analyze_changes(
store, abs_files, repo_root=str(root), base=base,
)
risk_score = analysis.get("risk_score", 0.0)
risk = (
"high" if risk_score > 0.7
else "medium" if risk_score > 0.4
else "low"
)
top_affected = [
f.get("name", "")
for f in analysis.get("changed_functions", [])[:5]
]
test_gap_count = len(analysis.get("test_gaps", []))
except (
ImportError, OSError, ValueError,
sqlite3.Error, subprocess.SubprocessError,
):
logger.debug("Risk analysis failed in get_minimal_context", exc_info=True)
# 3. Top 3 communities
communities: list[str] = []
try:
rows = store._conn.execute(
"SELECT name FROM communities ORDER BY size DESC LIMIT 3"
).fetchall()
communities = [r[0] for r in rows]
except sqlite3.OperationalError: # nosec B110 — table may not exist yet
logger.debug("communities table not yet populated")
# 4. Top 3 critical flows
flows: list[str] = []
try:
rows = store._conn.execute(
"SELECT name FROM flows ORDER BY criticality DESC LIMIT 3"
).fetchall()
flows = [r[0] for r in rows]
except sqlite3.OperationalError: # nosec B110 — table may not exist yet
logger.debug("flows table not yet populated")
# 5. Suggest next tools based on task keywords
task_lower = task.lower()
if any(w in task_lower for w in ("review", "pr", "merge", "diff")):
suggestions = ["detect_changes", "get_affected_flows", "get_review_context"]
elif any(w in task_lower for w in ("debug", "bug", "error", "fix")):
suggestions = ["semantic_search_nodes", "query_graph", "get_flow"]
elif any(w in task_lower for w in ("refactor", "rename", "dead", "clean")):
suggestions = ["refactor", "find_large_functions", "get_architecture_overview"]
elif any(w in task_lower for w in ("onboard", "understand", "explore", "arch")):
suggestions = [
"get_architecture_overview", "list_communities", "list_flows",
]
else:
suggestions = [
"detect_changes", "semantic_search_nodes",
"get_architecture_overview",
]
# Build summary
summary_parts = [
f"{stats.total_nodes} nodes, {stats.total_edges} edges"
f" across {stats.files_count} files.",
]
if risk != "unknown":
summary_parts.append(f"Risk: {risk} ({risk_score:.2f}).")
if test_gap_count:
summary_parts.append(f"{test_gap_count} test gaps.")
return compact_response(
summary=" ".join(summary_parts),
key_entities=top_affected or None,
risk=risk,
communities=communities or None,
flows_affected=flows or None,
next_tool_suggestions=suggestions,
)
finally:
store.close()