Spaces:
Sleeping
Sleeping
feat: add all 5 agents - analysis, bugs, tests, review, report
Browse files
app/agents/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.agents import (
|
| 2 |
+
repo_analysis_agent,
|
| 3 |
+
bug_detection_agent,
|
| 4 |
+
test_generation_agent,
|
| 5 |
+
code_review_agent,
|
| 6 |
+
report_generator_agent
|
| 7 |
+
)
|
app/agents/bug_detection_agent.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from app.tools.static_analyzer import run_full_analysis
|
| 3 |
+
from app.tools.pytest_runner import run_tests
|
| 4 |
+
from app.tools.file_scanner import get_python_files
|
| 5 |
+
from app.core.llm import call_llm
|
| 6 |
+
from app.core.prompts import BUG_DETECTION_PROMPT
|
| 7 |
+
from app.models.issue import IssueReport, Bug, Warning, Suggestion
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def run(local_path: str) -> IssueReport:
|
| 11 |
+
"""
|
| 12 |
+
Detect bugs, warnings and suggestions in a repository.
|
| 13 |
+
"""
|
| 14 |
+
print("Running Bug Detection Agent...")
|
| 15 |
+
|
| 16 |
+
# Step 1 - Run static analysis
|
| 17 |
+
static_results = run_full_analysis(local_path)
|
| 18 |
+
|
| 19 |
+
# Step 2 - Run tests
|
| 20 |
+
test_results = run_tests(local_path)
|
| 21 |
+
|
| 22 |
+
# Step 3 - Read sample of code for LLM
|
| 23 |
+
python_files = get_python_files(local_path)
|
| 24 |
+
code_samples = []
|
| 25 |
+
for file_path in python_files[:5]: # Limit to first 5 files
|
| 26 |
+
try:
|
| 27 |
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
| 28 |
+
content = f.read()[:2000] # Limit to 2000 chars per file
|
| 29 |
+
code_samples.append(f"### {file_path}\n{content}")
|
| 30 |
+
except Exception:
|
| 31 |
+
pass
|
| 32 |
+
|
| 33 |
+
# Step 4 - Build context for LLM
|
| 34 |
+
code_info = f"""
|
| 35 |
+
Static Analysis Results:
|
| 36 |
+
Ruff Issues: {len(static_results['ruff']['issues'])}
|
| 37 |
+
Bandit Issues: {len(static_results['bandit']['issues'])}
|
| 38 |
+
|
| 39 |
+
Ruff Output:
|
| 40 |
+
{chr(10).join(static_results['ruff']['issues'][:20])}
|
| 41 |
+
|
| 42 |
+
Bandit Output:
|
| 43 |
+
{chr(10).join(static_results['bandit']['issues'][:20])}
|
| 44 |
+
|
| 45 |
+
Test Results:
|
| 46 |
+
Passed: {test_results['passed']}
|
| 47 |
+
Failed: {test_results['failed']}
|
| 48 |
+
Errors: {test_results['errors']}
|
| 49 |
+
|
| 50 |
+
Code Samples:
|
| 51 |
+
{chr(10).join(code_samples)}
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
# Step 5 - Call LLM
|
| 55 |
+
prompt = BUG_DETECTION_PROMPT.format(code_info=code_info)
|
| 56 |
+
llm_response = call_llm(prompt)
|
| 57 |
+
|
| 58 |
+
# Step 6 - Parse response
|
| 59 |
+
try:
|
| 60 |
+
llm_data = json.loads(llm_response)
|
| 61 |
+
except json.JSONDecodeError:
|
| 62 |
+
llm_data = {}
|
| 63 |
+
|
| 64 |
+
# Step 7 - Build IssueReport
|
| 65 |
+
critical = [
|
| 66 |
+
Bug(**bug) if isinstance(bug, dict) else Bug(description=str(bug), file="unknown")
|
| 67 |
+
for bug in llm_data.get("critical", [])
|
| 68 |
+
]
|
| 69 |
+
warnings = [
|
| 70 |
+
Warning(**w) if isinstance(w, dict) else Warning(description=str(w), file="unknown")
|
| 71 |
+
for w in llm_data.get("warnings", [])
|
| 72 |
+
]
|
| 73 |
+
suggestions = [
|
| 74 |
+
Suggestion(**s) if isinstance(s, dict) else Suggestion(description=str(s), file="unknown")
|
| 75 |
+
for s in llm_data.get("suggestions", [])
|
| 76 |
+
]
|
| 77 |
+
|
| 78 |
+
report = IssueReport(
|
| 79 |
+
critical=critical,
|
| 80 |
+
warnings=warnings,
|
| 81 |
+
suggestions=suggestions
|
| 82 |
+
)
|
| 83 |
+
report.calculate_totals()
|
| 84 |
+
|
| 85 |
+
print(f"Bug detection complete: {report.total_critical} critical, {report.total_warnings} warnings")
|
| 86 |
+
return report
|
app/agents/code_review_agent.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from app.tools.file_scanner import get_python_files
|
| 3 |
+
from app.core.llm import call_llm
|
| 4 |
+
from app.core.prompts import CODE_REVIEW_PROMPT
|
| 5 |
+
from app.models.review import ReviewSuggestions
|
| 6 |
+
from app.models.repository import RepositoryMetadata
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def run(local_path: str, metadata: RepositoryMetadata) -> ReviewSuggestions:
|
| 10 |
+
"""
|
| 11 |
+
Perform AI-powered code review on repository.
|
| 12 |
+
"""
|
| 13 |
+
print("Running Code Review Agent...")
|
| 14 |
+
|
| 15 |
+
# Step 1 - Get Python files
|
| 16 |
+
python_files = get_python_files(local_path)
|
| 17 |
+
|
| 18 |
+
# Step 2 - Read source code samples
|
| 19 |
+
source_samples = []
|
| 20 |
+
for file_path in python_files[:4]:
|
| 21 |
+
if "test_" in file_path:
|
| 22 |
+
continue
|
| 23 |
+
try:
|
| 24 |
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
| 25 |
+
content = f.read()[:2500]
|
| 26 |
+
source_samples.append(f"### {file_path}\n{content}")
|
| 27 |
+
except Exception:
|
| 28 |
+
pass
|
| 29 |
+
|
| 30 |
+
if not source_samples:
|
| 31 |
+
return ReviewSuggestions(
|
| 32 |
+
summary="No source files found to review.",
|
| 33 |
+
overall_score=0.0
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# Step 3 - Call LLM
|
| 37 |
+
source_code = "\n\n".join(source_samples)
|
| 38 |
+
prompt = CODE_REVIEW_PROMPT.format(source_code=source_code)
|
| 39 |
+
llm_response = call_llm(prompt)
|
| 40 |
+
|
| 41 |
+
# Step 4 - Parse response
|
| 42 |
+
try:
|
| 43 |
+
llm_data = json.loads(llm_response)
|
| 44 |
+
except json.JSONDecodeError:
|
| 45 |
+
llm_data = {}
|
| 46 |
+
|
| 47 |
+
review = ReviewSuggestions(
|
| 48 |
+
solid_violations=llm_data.get("solid_violations", []),
|
| 49 |
+
duplicate_code=llm_data.get("duplicate_code", []),
|
| 50 |
+
refactor_suggestions=llm_data.get("refactor_suggestions", []),
|
| 51 |
+
overall_score=float(llm_data.get("overall_score", 5.0)),
|
| 52 |
+
summary=llm_data.get("summary", "")
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
print(f"Code review complete: score {review.overall_score}/10")
|
| 56 |
+
return review
|
app/agents/repo_analysis_agent.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from app.tools.file_scanner import scan_repository, get_entry_points
|
| 3 |
+
from app.tools.ast_parser import parse_repository
|
| 4 |
+
from app.core.llm import call_llm
|
| 5 |
+
from app.core.prompts import REPO_ANALYSIS_PROMPT
|
| 6 |
+
from app.models.repository import RepositoryMetadata
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def run(repo_url: str, local_path: str) -> RepositoryMetadata:
|
| 10 |
+
"""
|
| 11 |
+
Analyze a repository and return structured metadata.
|
| 12 |
+
"""
|
| 13 |
+
print("Running Repository Analysis Agent...")
|
| 14 |
+
|
| 15 |
+
# Step 1 - Scan files
|
| 16 |
+
scan_results = scan_repository(local_path)
|
| 17 |
+
entry_points = get_entry_points(local_path)
|
| 18 |
+
ast_results = parse_repository(local_path)
|
| 19 |
+
|
| 20 |
+
# Step 2 - Build context for LLM
|
| 21 |
+
repo_info = f"""
|
| 22 |
+
Repository URL: {repo_url}
|
| 23 |
+
Primary Language: {scan_results['primary_language']}
|
| 24 |
+
Total Files: {scan_results['total_files']}
|
| 25 |
+
Total Lines: {scan_results['total_lines']}
|
| 26 |
+
Languages Found: {scan_results['language_counts']}
|
| 27 |
+
Frameworks Detected: {scan_results['frameworks']}
|
| 28 |
+
Entry Points: {entry_points}
|
| 29 |
+
Classes Found: {sum(len(r['classes']) for r in ast_results)}
|
| 30 |
+
Functions Found: {sum(len(r['functions']) for r in ast_results)}
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
# Step 3 - Call LLM
|
| 34 |
+
prompt = REPO_ANALYSIS_PROMPT.format(repo_info=repo_info)
|
| 35 |
+
llm_response = call_llm(prompt)
|
| 36 |
+
|
| 37 |
+
# Step 4 - Parse response
|
| 38 |
+
try:
|
| 39 |
+
llm_data = json.loads(llm_response)
|
| 40 |
+
except json.JSONDecodeError:
|
| 41 |
+
llm_data = {}
|
| 42 |
+
|
| 43 |
+
# Step 5 - Build metadata
|
| 44 |
+
repo_name = repo_url.rstrip("/").split("/")[-1]
|
| 45 |
+
|
| 46 |
+
metadata = RepositoryMetadata(
|
| 47 |
+
url=repo_url,
|
| 48 |
+
name=repo_name,
|
| 49 |
+
local_path=local_path,
|
| 50 |
+
language=llm_data.get("language", scan_results["primary_language"]),
|
| 51 |
+
frameworks=llm_data.get("frameworks", scan_results["frameworks"]),
|
| 52 |
+
total_files=scan_results["total_files"],
|
| 53 |
+
total_lines=scan_results["total_lines"],
|
| 54 |
+
entry_points=[str(e) for e in entry_points],
|
| 55 |
+
architecture=llm_data.get("architecture", ""),
|
| 56 |
+
summary=llm_data.get("summary", "")
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
print(f"Analysis complete: {metadata.language}, {len(metadata.frameworks)} frameworks")
|
| 60 |
+
return metadata
|
app/agents/report_generator_agent.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from app.core.llm import call_llm
|
| 3 |
+
from app.core.prompts import REPORT_GENERATOR_PROMPT
|
| 4 |
+
from app.models.repository import RepositoryMetadata
|
| 5 |
+
from app.models.issue import IssueReport
|
| 6 |
+
from app.models.review import ReviewSuggestions, GeneratedTests
|
| 7 |
+
from app.models.report import EngineeringReport
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def run(
|
| 11 |
+
metadata: RepositoryMetadata,
|
| 12 |
+
issues: IssueReport,
|
| 13 |
+
tests: GeneratedTests,
|
| 14 |
+
review: ReviewSuggestions
|
| 15 |
+
) -> EngineeringReport:
|
| 16 |
+
"""
|
| 17 |
+
Generate final engineering report combining all agent outputs.
|
| 18 |
+
"""
|
| 19 |
+
print("Running Report Generator Agent...")
|
| 20 |
+
|
| 21 |
+
# Step 1 - Build context for LLM
|
| 22 |
+
analysis_summary = f"""
|
| 23 |
+
Repository: {metadata.name}
|
| 24 |
+
Language: {metadata.language}
|
| 25 |
+
Frameworks: {metadata.frameworks}
|
| 26 |
+
Total Files: {metadata.total_files}
|
| 27 |
+
Total Lines: {metadata.total_lines}
|
| 28 |
+
Summary: {metadata.summary}
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
bugs_summary = f"""
|
| 32 |
+
Critical Bugs: {issues.total_critical}
|
| 33 |
+
Warnings: {issues.total_warnings}
|
| 34 |
+
Suggestions: {issues.total_suggestions}
|
| 35 |
+
Critical Issues: {[b.description for b in issues.critical[:3]]}
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
tests_summary = f"""
|
| 39 |
+
Tests Generated: {tests.total_tests_generated}
|
| 40 |
+
Functions Covered: {tests.functions_covered[:5]}
|
| 41 |
+
Coverage Before: {tests.estimated_coverage_before}%
|
| 42 |
+
Coverage After: {tests.estimated_coverage_after}%
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
review_summary = f"""
|
| 46 |
+
Overall Score: {review.overall_score}/10
|
| 47 |
+
SOLID Violations: {review.solid_violations[:3]}
|
| 48 |
+
Refactor Suggestions: {review.refactor_suggestions[:3]}
|
| 49 |
+
Summary: {review.summary}
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
# Step 2 - Call LLM
|
| 53 |
+
prompt = REPORT_GENERATOR_PROMPT.format(
|
| 54 |
+
analysis=analysis_summary,
|
| 55 |
+
bugs=bugs_summary,
|
| 56 |
+
tests=tests_summary,
|
| 57 |
+
review=review_summary
|
| 58 |
+
)
|
| 59 |
+
full_report = call_llm(prompt, max_tokens=3000)
|
| 60 |
+
|
| 61 |
+
# Step 3 - Build final report
|
| 62 |
+
report = EngineeringReport(
|
| 63 |
+
repository=metadata,
|
| 64 |
+
issues=issues,
|
| 65 |
+
review=review,
|
| 66 |
+
tests=tests,
|
| 67 |
+
full_report=full_report,
|
| 68 |
+
generated_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
print("Report generation complete.")
|
| 72 |
+
return report
|
app/agents/test_generation_agent.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.tools.file_scanner import get_python_files
|
| 2 |
+
from app.tools.ast_parser import parse_python_file
|
| 3 |
+
from app.core.llm import call_llm
|
| 4 |
+
from app.core.prompts import TEST_GENERATION_PROMPT
|
| 5 |
+
from app.models.review import GeneratedTests
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def run(local_path: str) -> GeneratedTests:
|
| 9 |
+
"""
|
| 10 |
+
Generate pytest test cases for uncovered functions.
|
| 11 |
+
"""
|
| 12 |
+
print("Running Test Generation Agent...")
|
| 13 |
+
|
| 14 |
+
# Step 1 - Get Python files
|
| 15 |
+
python_files = get_python_files(local_path)
|
| 16 |
+
|
| 17 |
+
# Step 2 - Find functions without tests
|
| 18 |
+
all_functions = []
|
| 19 |
+
source_code_samples = []
|
| 20 |
+
|
| 21 |
+
for file_path in python_files:
|
| 22 |
+
# Skip test files
|
| 23 |
+
if "test_" in file_path or "_test" in file_path:
|
| 24 |
+
continue
|
| 25 |
+
|
| 26 |
+
parsed = parse_python_file(file_path)
|
| 27 |
+
functions = parsed.get("functions", [])
|
| 28 |
+
|
| 29 |
+
if functions:
|
| 30 |
+
all_functions.extend([f["name"] for f in functions])
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
| 34 |
+
content = f.read()[:3000]
|
| 35 |
+
source_code_samples.append(f"### {file_path}\n{content}")
|
| 36 |
+
except Exception:
|
| 37 |
+
pass
|
| 38 |
+
|
| 39 |
+
if len(source_code_samples) >= 3:
|
| 40 |
+
break
|
| 41 |
+
|
| 42 |
+
if not source_code_samples:
|
| 43 |
+
return GeneratedTests(
|
| 44 |
+
test_code="# No source files found to generate tests for",
|
| 45 |
+
total_tests_generated=0
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# Step 3 - Call LLM
|
| 49 |
+
source_code = "\n\n".join(source_code_samples)
|
| 50 |
+
prompt = TEST_GENERATION_PROMPT.format(source_code=source_code)
|
| 51 |
+
llm_response = call_llm(prompt, max_tokens=3000)
|
| 52 |
+
|
| 53 |
+
# Step 4 - Count generated tests
|
| 54 |
+
test_count = llm_response.count("def test_")
|
| 55 |
+
|
| 56 |
+
result = GeneratedTests(
|
| 57 |
+
test_code=llm_response,
|
| 58 |
+
functions_covered=all_functions[:test_count],
|
| 59 |
+
estimated_coverage_before=40.0,
|
| 60 |
+
estimated_coverage_after=min(40.0 + (test_count * 3), 95.0),
|
| 61 |
+
total_tests_generated=test_count
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
print(f"Test generation complete: {test_count} tests generated")
|
| 65 |
+
return result
|