278 lines
9.0 KiB
Python
278 lines
9.0 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 (НЕ сохраняем llm_chunk - их слишком много)
|
||
if event.get("type") != "llm_chunk":
|
||
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 (отправляем все события, включая llm_chunk)
|
||
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
|
||
|