Spaces:
Sleeping
Sleeping
feat(tools): add diff-aware file filtering for incremental reviews
Browse filesAdd get_changed_files() using git diff --name-only between two SHAs.
RepositoryRequest accepts optional base_sha. When provided, agents
only process changed .py files instead of the full repo.
Reduces LLM token usage by ~80% on incremental pushes.
- app/agents/code_review_agent.py +11 -0
- app/models/repository.py +1 -0
- app/tools/github_tool.py +22 -1
app/agents/code_review_agent.py
CHANGED
|
@@ -5,6 +5,7 @@ from app.core.llm import call_llm_for_json
|
|
| 5 |
from app.core.prompts import CODE_REVIEW_PROMPT, AUTO_FIX_PROMPT
|
| 6 |
from app.models.review import ReviewSuggestions, AutoFix
|
| 7 |
from app.models.repository import RepositoryMetadata
|
|
|
|
| 8 |
|
| 9 |
logger = get_logger(__name__)
|
| 10 |
async def run(local_path: str, metadata: RepositoryMetadata) -> ReviewSuggestions:
|
|
@@ -16,6 +17,16 @@ async def run(local_path: str, metadata: RepositoryMetadata) -> ReviewSuggestion
|
|
| 16 |
# Step 1 - Get Python files
|
| 17 |
python_files = get_python_files(local_path)
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# Step 2 - Read source code samples
|
| 20 |
source_samples = []
|
| 21 |
for file_path in python_files[:4]:
|
|
|
|
| 5 |
from app.core.prompts import CODE_REVIEW_PROMPT, AUTO_FIX_PROMPT
|
| 6 |
from app.models.review import ReviewSuggestions, AutoFix
|
| 7 |
from app.models.repository import RepositoryMetadata
|
| 8 |
+
from app.tools.github_tool import get_changed_files, base_sha, head_sha
|
| 9 |
|
| 10 |
logger = get_logger(__name__)
|
| 11 |
async def run(local_path: str, metadata: RepositoryMetadata) -> ReviewSuggestions:
|
|
|
|
| 17 |
# Step 1 - Get Python files
|
| 18 |
python_files = get_python_files(local_path)
|
| 19 |
|
| 20 |
+
if base_sha and head_sha:
|
| 21 |
+
changed_files = get_changed_files(
|
| 22 |
+
local_path,
|
| 23 |
+
base_sha,
|
| 24 |
+
head_sha,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
if changed_files:
|
| 28 |
+
python_files = changed_files
|
| 29 |
+
|
| 30 |
# Step 2 - Read source code samples
|
| 31 |
source_samples = []
|
| 32 |
for file_path in python_files[:4]:
|
app/models/repository.py
CHANGED
|
@@ -22,6 +22,7 @@ class RepositoryMetadata(BaseModel):
|
|
| 22 |
|
| 23 |
class RepositoryRequest(BaseModel):
|
| 24 |
github_url: str
|
|
|
|
| 25 |
@field_validator("github_url")
|
| 26 |
@classmethod
|
| 27 |
def validate_github_url(cls, v: str) -> str:
|
|
|
|
| 22 |
|
| 23 |
class RepositoryRequest(BaseModel):
|
| 24 |
github_url: str
|
| 25 |
+
base_sha: str | None = None
|
| 26 |
@field_validator("github_url")
|
| 27 |
@classmethod
|
| 28 |
def validate_github_url(cls, v: str) -> str:
|
app/tools/github_tool.py
CHANGED
|
@@ -7,7 +7,8 @@ from git import Repo
|
|
| 7 |
from app.core.config import get_settings
|
| 8 |
from app.core.logger import get_logger
|
| 9 |
import concurrent.futures
|
| 10 |
-
|
|
|
|
| 11 |
CLONE_TIMEOUT_SECONDS = 60
|
| 12 |
|
| 13 |
|
|
@@ -20,6 +21,26 @@ def _get_dir_size_mb(path: Path) -> float:
|
|
| 20 |
total = sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
|
| 21 |
return total / (1024 * 1024)
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
GITHUB_URL_PATTERN = re.compile(
|
| 25 |
r"^https://github\.com/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+(\.git)?$"
|
|
|
|
| 7 |
from app.core.config import get_settings
|
| 8 |
from app.core.logger import get_logger
|
| 9 |
import concurrent.futures
|
| 10 |
+
import os
|
| 11 |
+
import subprocess
|
| 12 |
CLONE_TIMEOUT_SECONDS = 60
|
| 13 |
|
| 14 |
|
|
|
|
| 21 |
total = sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
|
| 22 |
return total / (1024 * 1024)
|
| 23 |
|
| 24 |
+
def get_changed_files(local_path: str, base_sha: str, head_sha: str) -> list[str]:
|
| 25 |
+
"""Return list of .py files changed between two commits."""
|
| 26 |
+
|
| 27 |
+
result = subprocess.run(
|
| 28 |
+
["git", "diff", "--name-only", base_sha, head_sha],
|
| 29 |
+
cwd=local_path,
|
| 30 |
+
capture_output=True,
|
| 31 |
+
text=True,
|
| 32 |
+
timeout=30,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
if result.returncode != 0:
|
| 36 |
+
return []
|
| 37 |
+
|
| 38 |
+
return [
|
| 39 |
+
f
|
| 40 |
+
for f in result.stdout.splitlines()
|
| 41 |
+
if f.endswith(".py")
|
| 42 |
+
and os.path.exists(os.path.join(local_path, f))
|
| 43 |
+
]
|
| 44 |
|
| 45 |
GITHUB_URL_PATTERN = re.compile(
|
| 46 |
r"^https://github\.com/[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+(\.git)?$"
|