Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion backend/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ class FaceVerifyRequest(BaseModel):
class ResolveAlertRequest(BaseModel):
conflict_id: str
approved: bool = False
reviewer_id: Optional[str] = None
reason: Optional[str] = None

class AdminReviewRequest(BaseModel):
conflict_id: str
approved: bool
reviewer_id: str
reason: str

# Initialize the main orchestrator agent
main_agent = MainAgent()
Expand Down Expand Up @@ -60,9 +68,37 @@ async def get_pending_alerts():

@app.post("/face/resolve-alert")
async def resolve_alert(request: ResolveAlertRequest):
success = face_service.resolve_alert(request.conflict_id, request.approved)
success = face_service.resolve_alert(
request.conflict_id,
request.approved,
request.reviewer_id,
request.reason
)
return {"success": success}

@app.get("/face/conflict/{conflict_id}")
async def get_conflict_details(conflict_id: str):
details = face_service.get_conflict_details(conflict_id)
if details is None:
return {"error": "Conflict not found"}, 404
return details
Comment on lines +79 to +84
Comment on lines +79 to +84

@app.post("/admin/review-conflict")
async def admin_review_conflict(request: AdminReviewRequest):
success = face_service.admin_review_conflict(
request.conflict_id,
request.approved,
request.reviewer_id,
request.reason
)
if not success:
return {"error": "Conflict not found"}, 404
return {"success": True, "message": "Review decision recorded"}
Comment on lines +86 to +96

@app.get("/admin/override-log")
async def get_override_log():
return face_service.get_override_log()
Comment on lines +86 to +100

@app.post("/analyze")
async def analyze_repo(request: AnalyzeRequest):
# Legacy REST endpoint for backward compatibility
Expand Down
6 changes: 6 additions & 0 deletions backend/src/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ class EventType(str, Enum):
MANUAL_REVIEW_REQUIRED = "manual_review.required"
ACCESS_DENIED_CONFLICT = "access.denied_conflict"

# Admin Review Events
ADMIN_REVIEW_STARTED = "admin_review.started"
ADMIN_REVIEW_APPROVED = "admin_review.approved"
ADMIN_REVIEW_REJECTED = "admin_review.rejected"
ADMIN_OVERRIDE_ACTION = "admin_override.action"

# ORACLE Events
FILE_RECEIVED = "file_received"
PDF_PARSED = "pdf_parsed"
Expand Down
51 changes: 49 additions & 2 deletions backend/src/services/face_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class ConflictAlert:
timestamp: datetime
status: str = "pending_review"
session_id: Optional[str] = None
reviewer_id: Optional[str] = None
review_timestamp: Optional[datetime] = None
review_reason: Optional[str] = None

def to_dict(self) -> Dict[str, Any]:
return {
Expand All @@ -36,6 +39,9 @@ def to_dict(self) -> Dict[str, Any]:
"timestamp": self.timestamp.isoformat(),
"status": self.status,
"session_id": self.session_id,
"reviewer_id": self.reviewer_id,
"review_timestamp": self.review_timestamp.isoformat() if self.review_timestamp else None,
"review_reason": self.review_reason,
}


Expand Down Expand Up @@ -124,10 +130,13 @@ def get_conflicts_for_roll(self, roll_number: str) -> List[ConflictAlert]:
def get_pending_alerts(self) -> List[ConflictAlert]:
return [alert for alert in self.alerts if alert.status == "pending_review"]

def resolve_alert(self, conflict_id: str, approved: bool) -> bool:
def resolve_alert(self, conflict_id: str, approved: bool, reviewer_id: Optional[str] = None, reason: Optional[str] = None) -> bool:
for alert in self.alerts:
if alert.conflict_id == conflict_id:
alert.status = "approved" if approved else "rejected"
alert.reviewer_id = reviewer_id
alert.review_timestamp = datetime.now(timezone.utc)
alert.review_reason = reason
return True
Comment on lines +133 to 140
return False

Expand All @@ -147,4 +156,42 @@ def can_grant_access(self, roll_number: str) -> Tuple[bool, Optional[str]]:
for conflict in conflicts:
if conflict.status == "pending_review":
return False, f"Identity under review: conflict {conflict.conflict_id}"
return True, None
return True, None

def get_conflict_details(self, conflict_id: str) -> Optional[Dict[str, Any]]:
for alert in self.alerts:
if alert.conflict_id == conflict_id:
prior_embeddings = []
for roll in alert.matched_roll_numbers:
if roll in self.embeddings:
emb = self.embeddings[roll]
prior_embeddings.append({
"roll_number": emb.roll_number,
"student_name": emb.student_name,
"timestamp": emb.timestamp.isoformat(),
"session_id": emb.session_id,
})
return {
"conflict": alert.to_dict(),
"prior_embeddings": prior_embeddings,
}
return None

def admin_review_conflict(self, conflict_id: str, approved: bool, reviewer_id: str, reason: str) -> bool:
for alert in self.alerts:
if alert.conflict_id == conflict_id:
alert.status = "approved" if approved else "rejected"
alert.reviewer_id = reviewer_id
alert.review_timestamp = datetime.now(timezone.utc)
alert.review_reason = reason
return True
return False
Comment on lines +181 to +188

def get_override_log(self) -> List[Dict[str, Any]]:
return [alert.to_dict() for alert in self.alerts if alert.status in ("approved", "rejected")]

Comment on lines +190 to +192
def get_alert_by_id(self, conflict_id: str) -> Optional[ConflictAlert]:
for alert in self.alerts:
if alert.conflict_id == conflict_id:
return alert
return None
96 changes: 94 additions & 2 deletions backend/tests/test_face_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,12 @@ def test_resolve_alert(self):
service.verify_identity(emb1, "R001")
_, alert, _ = service.verify_identity(emb2, "R002")

result = service.resolve_alert(alert.conflict_id, approved=True)
result = service.resolve_alert(alert.conflict_id, approved=True, reviewer_id="admin-1", reason="test resolution")

assert result is True
assert alert.status == "approved"
assert alert.reviewer_id == "admin-1"
assert alert.review_reason == "test resolution"

def test_get_suspicious_identities(self):
service = FaceDetectionService()
Expand Down Expand Up @@ -168,4 +170,94 @@ def test_multi_roll_number_conflict(self):
is_valid, alert, _ = service.verify_identity(emb3, "R003")

assert is_valid is False
assert len(alert.matched_roll_numbers) >= 1
assert len(alert.matched_roll_numbers) >= 1

def test_get_conflict_details(self):
service = FaceDetectionService()
emb1 = generate_embedding(base=0.5)
emb2 = create_similar_embedding(emb1, 0.92)
Comment on lines +175 to +178

service.add_embedding(emb1, "R001", "Alice")
_, alert, _ = service.verify_identity(emb2, "R002", "session-2")

details = service.get_conflict_details(alert.conflict_id)

assert details is not None
assert "conflict" in details
assert "prior_embeddings" in details
assert len(details["prior_embeddings"]) == 1
assert details["prior_embeddings"][0]["roll_number"] == "R001"
assert details["prior_embeddings"][0]["student_name"] == "Alice"

def test_admin_review_conflict(self):
service = FaceDetectionService()
emb1 = generate_embedding(base=0.5)
emb2 = create_similar_embedding(emb1, 0.92)

service.verify_identity(emb1, "R001")
_, alert, _ = service.verify_identity(emb2, "R002")

result = service.admin_review_conflict(
alert.conflict_id,
approved=True,
reviewer_id="admin-1",
reason="Verified as same person"
)

assert result is True
assert alert.status == "approved"
assert alert.reviewer_id == "admin-1"
assert alert.review_reason == "Verified as same person"
assert alert.review_timestamp is not None

def test_admin_review_rejection(self):
service = FaceDetectionService()
emb1 = generate_embedding(base=0.5)
emb2 = create_similar_embedding(emb1, 0.92)

service.verify_identity(emb1, "R001")
_, alert, _ = service.verify_identity(emb2, "R002")

result = service.admin_review_conflict(
alert.conflict_id,
approved=False,
reviewer_id="admin-2",
reason="Different people, access denied"
)

assert result is True
assert alert.status == "rejected"
assert alert.reviewer_id == "admin-2"

def test_get_override_log(self):
service = FaceDetectionService()
emb1 = generate_embedding(base=0.5)
emb2 = create_similar_embedding(emb1, 0.92)
emb3 = create_similar_embedding(emb1, 0.90)

service.verify_identity(emb1, "R001")
_, alert1, _ = service.verify_identity(emb2, "R002")
_, alert2, _ = service.verify_identity(emb3, "R003")

service.admin_review_conflict(alert1.conflict_id, True, "admin-1", "approved")
service.admin_review_conflict(alert2.conflict_id, False, "admin-2", "rejected")

override_log = service.get_override_log()

assert len(override_log) == 2
assert override_log[0]["status"] in ("approved", "rejected")
assert override_log[0]["reviewer_id"] == "admin-1"

def test_get_alert_by_id(self):
service = FaceDetectionService()
emb1 = generate_embedding(base=0.5)
emb2 = create_similar_embedding(emb1, 0.92)

service.verify_identity(emb1, "R001")
_, alert, _ = service.verify_identity(emb2, "R002")

found = service.get_alert_by_id(alert.conflict_id)
not_found = service.get_alert_by_id("nonexistent")

assert found is alert
assert not_found is None
Loading