277 lines
8.8 KiB
Python
277 lines
8.8 KiB
Python
"""Review management endpoints"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func
|
|
from sqlalchemy.orm import joinedload
|
|
|
|
from app.database import get_db
|
|
from app.models import Review, Comment, PullRequest, ReviewEvent
|
|
from app.schemas.review import ReviewResponse, ReviewList, ReviewStats, PullRequestInfo, CommentResponse
|
|
from app.schemas.review_event import ReviewEvent as ReviewEventSchema
|
|
from app.agents import ReviewerAgent
|
|
from typing import List
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("", response_model=ReviewList)
|
|
async def list_reviews(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
repository_id: int = None,
|
|
status: str = None,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""List all reviews with filters"""
|
|
query = select(Review).options(joinedload(Review.pull_request))
|
|
|
|
# Apply filters
|
|
if repository_id:
|
|
query = query.join(PullRequest).where(PullRequest.repository_id == repository_id)
|
|
|
|
if status:
|
|
query = query.where(Review.status == status)
|
|
|
|
# Get total count
|
|
count_query = select(func.count(Review.id))
|
|
if repository_id:
|
|
count_query = count_query.join(PullRequest).where(PullRequest.repository_id == repository_id)
|
|
if status:
|
|
count_query = count_query.where(Review.status == status)
|
|
|
|
count_result = await db.execute(count_query)
|
|
total = count_result.scalar()
|
|
|
|
# Get reviews
|
|
query = query.offset(skip).limit(limit).order_by(Review.started_at.desc())
|
|
result = await db.execute(query)
|
|
reviews = result.scalars().all()
|
|
|
|
# Convert to response models
|
|
items = []
|
|
for review in reviews:
|
|
pr_info = PullRequestInfo(
|
|
id=review.pull_request.id,
|
|
pr_number=review.pull_request.pr_number,
|
|
title=review.pull_request.title,
|
|
author=review.pull_request.author,
|
|
source_branch=review.pull_request.source_branch,
|
|
target_branch=review.pull_request.target_branch,
|
|
url=review.pull_request.url
|
|
)
|
|
|
|
items.append(ReviewResponse(
|
|
id=review.id,
|
|
pull_request_id=review.pull_request_id,
|
|
pull_request=pr_info,
|
|
status=review.status,
|
|
started_at=review.started_at,
|
|
completed_at=review.completed_at,
|
|
files_analyzed=review.files_analyzed,
|
|
comments_generated=review.comments_generated,
|
|
error_message=review.error_message
|
|
))
|
|
|
|
return ReviewList(items=items, total=total)
|
|
|
|
|
|
@router.get("/{review_id}", response_model=ReviewResponse)
|
|
async def get_review(
|
|
review_id: int,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get review by ID with comments"""
|
|
result = await db.execute(
|
|
select(Review)
|
|
.options(joinedload(Review.pull_request), joinedload(Review.comments))
|
|
.where(Review.id == review_id)
|
|
)
|
|
review = result.unique().scalar_one_or_none()
|
|
|
|
if not review:
|
|
raise HTTPException(status_code=404, detail="Review not found")
|
|
|
|
pr_info = PullRequestInfo(
|
|
id=review.pull_request.id,
|
|
pr_number=review.pull_request.pr_number,
|
|
title=review.pull_request.title,
|
|
author=review.pull_request.author,
|
|
source_branch=review.pull_request.source_branch,
|
|
target_branch=review.pull_request.target_branch,
|
|
url=review.pull_request.url
|
|
)
|
|
|
|
comments = [
|
|
CommentResponse(
|
|
id=comment.id,
|
|
file_path=comment.file_path,
|
|
line_number=comment.line_number,
|
|
content=comment.content,
|
|
severity=comment.severity,
|
|
posted=comment.posted,
|
|
posted_at=comment.posted_at,
|
|
created_at=comment.created_at
|
|
)
|
|
for comment in review.comments
|
|
]
|
|
|
|
return ReviewResponse(
|
|
id=review.id,
|
|
pull_request_id=review.pull_request_id,
|
|
pull_request=pr_info,
|
|
status=review.status,
|
|
started_at=review.started_at,
|
|
completed_at=review.completed_at,
|
|
files_analyzed=review.files_analyzed,
|
|
comments_generated=review.comments_generated,
|
|
error_message=review.error_message,
|
|
comments=comments
|
|
)
|
|
|
|
|
|
async def run_review_task(review_id: int, pr_number: int, repository_id: int, db: AsyncSession):
|
|
"""Background task to run review with streaming"""
|
|
from app.main import manager
|
|
from datetime import datetime as dt
|
|
|
|
# Create event handler for streaming
|
|
async def on_review_event(event: dict):
|
|
"""Handle review events and broadcast to clients"""
|
|
try:
|
|
event_data = {
|
|
"type": event.get("type", "agent_update"),
|
|
"review_id": review_id,
|
|
"pr_number": pr_number,
|
|
"timestamp": dt.utcnow().isoformat(),
|
|
"data": event
|
|
}
|
|
|
|
# Save to DB
|
|
from app.models.review_event import ReviewEvent
|
|
db_event = ReviewEvent(
|
|
review_id=review_id,
|
|
event_type=event.get("type", "agent_update"),
|
|
step=event.get("step"),
|
|
message=event.get("message"),
|
|
data=event
|
|
)
|
|
db.add(db_event)
|
|
await db.commit()
|
|
|
|
# Broadcast
|
|
await manager.broadcast(event_data)
|
|
except Exception as e:
|
|
print(f"Error in review event handler: {e}")
|
|
|
|
agent = ReviewerAgent(db)
|
|
await agent.run_review_stream(review_id, pr_number, repository_id, on_event=on_review_event)
|
|
|
|
|
|
@router.post("/{review_id}/retry")
|
|
async def retry_review(
|
|
review_id: int,
|
|
background_tasks: BackgroundTasks,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Retry a failed review"""
|
|
result = await db.execute(
|
|
select(Review).options(joinedload(Review.pull_request)).where(Review.id == review_id)
|
|
)
|
|
review = result.scalar_one_or_none()
|
|
|
|
if not review:
|
|
raise HTTPException(status_code=404, detail="Review not found")
|
|
|
|
# Reset review status
|
|
from app.models.review import ReviewStatusEnum
|
|
review.status = ReviewStatusEnum.PENDING
|
|
review.error_message = None
|
|
await db.commit()
|
|
|
|
# Run review in background
|
|
background_tasks.add_task(
|
|
run_review_task,
|
|
review.id,
|
|
review.pull_request.pr_number,
|
|
review.pull_request.repository_id,
|
|
db
|
|
)
|
|
|
|
return {"message": "Review queued"}
|
|
|
|
|
|
@router.get("/stats/dashboard", response_model=ReviewStats)
|
|
async def get_review_stats(db: AsyncSession = Depends(get_db)):
|
|
"""Get review statistics for dashboard"""
|
|
# Total reviews
|
|
total_result = await db.execute(select(func.count(Review.id)))
|
|
total_reviews = total_result.scalar()
|
|
|
|
# Active reviews
|
|
from app.models.review import ReviewStatusEnum
|
|
active_result = await db.execute(
|
|
select(func.count(Review.id)).where(
|
|
Review.status.in_([
|
|
ReviewStatusEnum.PENDING,
|
|
ReviewStatusEnum.FETCHING,
|
|
ReviewStatusEnum.ANALYZING,
|
|
ReviewStatusEnum.COMMENTING
|
|
])
|
|
)
|
|
)
|
|
active_reviews = active_result.scalar()
|
|
|
|
# Completed reviews
|
|
completed_result = await db.execute(
|
|
select(func.count(Review.id)).where(Review.status == ReviewStatusEnum.COMPLETED)
|
|
)
|
|
completed_reviews = completed_result.scalar()
|
|
|
|
# Failed reviews
|
|
failed_result = await db.execute(
|
|
select(func.count(Review.id)).where(Review.status == ReviewStatusEnum.FAILED)
|
|
)
|
|
failed_reviews = failed_result.scalar()
|
|
|
|
# Total comments
|
|
comments_result = await db.execute(select(func.count(Comment.id)))
|
|
total_comments = comments_result.scalar()
|
|
|
|
# Average comments per review
|
|
avg_comments = total_comments / total_reviews if total_reviews > 0 else 0
|
|
|
|
return ReviewStats(
|
|
total_reviews=total_reviews,
|
|
active_reviews=active_reviews,
|
|
completed_reviews=completed_reviews,
|
|
failed_reviews=failed_reviews,
|
|
total_comments=total_comments,
|
|
avg_comments_per_review=round(avg_comments, 2)
|
|
)
|
|
|
|
|
|
@router.get("/{review_id}/events", response_model=List[ReviewEventSchema])
|
|
async def get_review_events(
|
|
review_id: int,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Get all events for a specific review"""
|
|
# Check if review exists
|
|
result = await db.execute(select(Review).where(Review.id == review_id))
|
|
review = result.scalar_one_or_none()
|
|
|
|
if not review:
|
|
raise HTTPException(status_code=404, detail="Review not found")
|
|
|
|
# Get events
|
|
events_result = await db.execute(
|
|
select(ReviewEvent)
|
|
.where(ReviewEvent.review_id == review_id)
|
|
.order_by(ReviewEvent.created_at)
|
|
)
|
|
events = events_result.scalars().all()
|
|
|
|
return events
|
|
|