diff --git a/backend/app/services/review_service.py b/backend/app/services/review_service.py index aa9d83b..1edb6ba 100644 --- a/backend/app/services/review_service.py +++ b/backend/app/services/review_service.py @@ -1,13 +1,20 @@ -"""Review service: HITL approval/rejection flow.""" +"""Review service: HITL approval/rejection flow with full context.""" from sqlalchemy.orm import Session +from sqlalchemy import func from typing import List, Optional from datetime import datetime from app.models.review import Review from app.models.task import Task -from app.schemas.review import ReviewCreate, ReviewDecision +from app.models.project import Project +from app.models.worker import AIWorker +from app.models.cost import CostLog +from app.schemas.review import ReviewCreate, ReviewDecision, ReviewDetailResponse class ReviewService: + + # ---- Basic CRUD ---- + @staticmethod def list_reviews( db: Session, @@ -51,6 +58,100 @@ class ReviewService: db.refresh(review) return review + # ---- Detail with full context ---- + + @staticmethod + def list_reviews_detail( + db: Session, + tenant_id: int, + status: Optional[str] = None, + project_id: Optional[int] = None, + skip: int = 0, + limit: int = 100, + ) -> List[ReviewDetailResponse]: + """List reviews with joined task/project/worker/cost context.""" + reviews = ReviewService.list_reviews( + db, tenant_id, status, project_id, skip, limit + ) + return [ReviewService._enrich(db, r) for r in reviews] + + @staticmethod + def get_review_detail( + db: Session, tenant_id: int, review_id: int + ) -> Optional[ReviewDetailResponse]: + """Get a single review with full context.""" + review = ReviewService.get_review(db, tenant_id, review_id) + if not review: + return None + return ReviewService._enrich(db, review) + + @staticmethod + def _enrich(db: Session, review: Review) -> ReviewDetailResponse: + """Join review with task, project, worker, and aggregate cost.""" + task = ( + db.query(Task) + .filter(Task.id == review.task_id, Task.tenant_id == review.tenant_id) + .first() + ) + project = ( + db.query(Project) + .filter(Project.id == review.project_id, Project.tenant_id == review.tenant_id) + .first() + ) + worker = None + if task and task.worker_id: + worker = ( + db.query(AIWorker) + .filter(AIWorker.id == task.worker_id, AIWorker.tenant_id == review.tenant_id) + .first() + ) + + # Aggregate cost data for this task + cost_row = ( + db.query( + func.sum(CostLog.cost_cents).label("total_cost"), + func.sum(CostLog.total_tokens).label("total_tokens"), + func.sum(CostLog.duration_ms).label("total_duration"), + ) + .filter(CostLog.task_id == review.task_id) + .first() + ) + + return ReviewDetailResponse( + id=review.id, + task_id=review.task_id, + project_id=review.project_id, + review_type=review.review_type, + status=review.status, + reviewer_id=review.reviewer_id, + reviewer_role=review.reviewer_role, + review_content=review.review_content, + reviewer_comment=review.reviewer_comment, + auto_check_result=review.auto_check_result, + submitted_at=review.submitted_at, + reviewed_at=review.reviewed_at, + created_at=review.created_at, + task_title=task.title if task else "", + task_description=task.description if task else "", + task_input_data=task.input_data if task else "", + task_output_data=task.output_data if task else "", + task_priority=task.priority if task else "medium", + task_type=task.task_type if task else "single_call", + task_status=task.status if task else "", + project_name=project.name if project else "", + project_description=project.description if project else "", + worker_id=worker.id if worker else None, + worker_name=worker.name if worker else None, + worker_model_name=worker.model_name if worker else None, + worker_provider=worker.provider if worker else None, + worker_system_prompt=worker.system_prompt if worker else None, + cost_cents=cost_row.total_cost or 0 if cost_row else 0, + token_usage=cost_row.total_tokens or 0 if cost_row else 0, + duration_ms=cost_row.total_duration or 0 if cost_row else 0, + ) + + # ---- Decision ---- + @staticmethod def make_decision( db: Session, @@ -72,7 +173,6 @@ class ReviewService: review.reviewer_comment = req.comment review.reviewed_at = datetime.utcnow() - # Update task status task = ( db.query(Task) .filter(Task.id == review.task_id, Task.tenant_id == tenant_id)