update the codebase poc ver1

This commit is contained in:
DatTT127
2026-07-07 15:54:17 +07:00
parent fed5f277f4
commit 1622dc8fc5
452 changed files with 83999 additions and 66328 deletions

View File

@@ -0,0 +1,233 @@
import asyncio
import logging
from contextlib import asynccontextmanager
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from fastapi.security import OAuth2PasswordBearer
from fastapi.responses import StreamingResponse
from datetime import datetime
from data.spec.schemas import (
AnalysisJobSubmit, JobStatus, PipelineStep, StepEvent,
ModelCatalog, ModelRegistrationResult, HealthStatus,
AnalysisJobSyncSubmit, JobResult, ErrorResponse,
)
from backend.implementation.analysis_jobs import service as analysis_service
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1", tags=["analysis"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
_event_queues: dict[str, asyncio.Queue] = {}
_queue_lock = asyncio.Lock()
@asynccontextmanager
async def _get_queue(job_id: str):
async with _queue_lock:
if job_id not in _event_queues:
_event_queues[job_id] = asyncio.Queue()
yield _event_queues[job_id]
def _get_queue_sync(job_id: str) -> asyncio.Queue:
if job_id not in _event_queues:
_event_queues[job_id] = asyncio.Queue()
return _event_queues[job_id]
async def _verify_jwt_token(token: str = Depends(oauth2_scheme)) -> str:
try:
from backend.api.auth_api import verify_jwt_token as _verify
return await _verify(token)
except HTTPException:
raise
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
def _sse_format(event: StepEvent) -> str:
lines = [f"event: {event.event_type}"]
payload = event.model_dump(mode="json")
lines.append(f"data: {payload}")
lines.append("")
lines.append("")
return "\n".join(lines)
@router.post(
"/analysis-jobs",
response_model=dict,
status_code=status.HTTP_201_CREATED,
responses={401: {"model": ErrorResponse}, 422: {"model": ErrorResponse}},
)
async def submit_analysis_job(
payload: AnalysisJobSubmit,
user_id: str = Depends(_verify_jwt_token),
):
try:
job_id = await analysis_service.submit_job(
session_id=payload.session_id,
params=payload.params or {},
model_versions=payload.model_versions,
)
return {"job_id": job_id}
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
@router.get(
"/analysis-jobs/{job_id}",
response_model=JobStatus,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def get_job_status(job_id: str, user_id: str = Depends(_verify_jwt_token)):
try:
return await analysis_service.job_status(job_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.get(
"/analysis-jobs/{job_id}/steps",
response_model=list[PipelineStep],
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def get_job_steps(job_id: str, user_id: str = Depends(_verify_jwt_token)):
try:
return await analysis_service.job_steps(job_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/analysis",
response_model=JobResult,
responses={401: {"model": ErrorResponse}, 422: {"model": ErrorResponse}},
)
async def submit_sync_analysis(
payload: AnalysisJobSyncSubmit,
user_id: str = Depends(_verify_jwt_token),
):
try:
return await analysis_service.submit_sync(
session_id=payload.session_id,
params=payload.params or {},
model_versions=payload.model_versions,
)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
@router.get(
"/analysis-jobs/{job_id}/stream",
responses={
401: {"model": ErrorResponse},
404: {"model": ErrorResponse},
},
)
async def stream_job_events(job_id: str, user_id: str = Depends(_verify_jwt_token)):
queue = _get_queue_sync(job_id)
async def event_generator():
try:
while True:
event = await queue.get()
yield _sse_format(event)
if event.event_type == "completed" or event.event_type == "failed":
break
except asyncio.CancelledError:
logger.info(f"SSE stream cancelled for job_id={job_id}")
finally:
async with _queue_lock:
_event_queues.pop(job_id, None)
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
"Connection": "keep-alive",
},
)
@router.post(
"/internal/analysis-jobs/{job_id}/events",
status_code=status.HTTP_202_ACCEPTED,
include_in_schema=False,
)
async def internal_push_event(job_id: str, event: dict):
queue = _get_queue_sync(job_id)
try:
step_event = StepEvent(
step_id=event.get("step_id", ""),
job_id=job_id,
event_type=event.get("event_type", "progress"),
task_type=event.get("task_type", ""),
status=event.get("status", "running"),
data=event.get("data"),
timestamp=datetime.now(),
)
await queue.put(step_event)
except Exception as exc:
logger.error(f"Failed to push event for job {job_id}: {exc}")
return {"queued": True}
@router.get(
"/health",
response_model=HealthStatus,
include_in_schema=False,
)
async def health_check():
try:
return await analysis_service.health()
except NotImplementedError:
return HealthStatus(
status="ok",
version="0.1.0",
dependencies={},
uptime_seconds=0.0,
)
@router.get(
"/model-registry",
response_model=ModelCatalog,
responses={401: {"model": ErrorResponse}},
)
async def list_models(user_id: str = Depends(_verify_jwt_token)):
try:
return await analysis_service.list_registered_models()
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
@router.post(
"/models/register",
response_model=ModelRegistrationResult,
status_code=status.HTTP_201_CREATED,
responses={401: {"model": ErrorResponse}},
)
async def register_model(
model_id: str,
file: UploadFile | None = None,
user_id: str = Depends(_verify_jwt_token),
):
try:
return await analysis_service.register_model(model_id, file)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))

View File

@@ -0,0 +1,82 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from data.spec.schemas import LoginRequest, Token, UserProfile, UserUpdateRequest, RefreshRequest, ErrorResponse
from backend.implementation.auth import service as auth_service
router = APIRouter(prefix="/api/v1", tags=["auth"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def verify_jwt_token(token: str = Depends(oauth2_scheme)) -> str:
try:
profile = await auth_service.me(token)
return profile.user_id
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
@router.post(
"/auth/login",
response_model=Token,
responses={401: {"model": ErrorResponse}},
)
async def login(payload: LoginRequest):
try:
return await auth_service.login(payload.username, payload.password)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc))
@router.post(
"/auth/logout",
status_code=status.HTTP_204_NO_CONTENT,
)
async def logout(token: str = Depends(oauth2_scheme)):
try:
await auth_service.logout(token)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
@router.post(
"/auth/refresh",
response_model=Token,
responses={401: {"model": ErrorResponse}},
)
async def refresh(payload: RefreshRequest):
try:
return await auth_service.refresh(payload.refresh_token)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc))
@router.get(
"/users/me",
response_model=UserProfile,
responses={401: {"model": ErrorResponse}},
)
async def get_me(user_id: str = Depends(verify_jwt_token)):
try:
return await auth_service.me(user_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
@router.patch(
"/users/me",
response_model=UserProfile,
responses={401: {"model": ErrorResponse}},
)
async def update_me(payload: UserUpdateRequest, user_id: str = Depends(verify_jwt_token)):
try:
return await auth_service.update_me(user_id, payload.model_dump(exclude_none=True))
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))

View File

@@ -0,0 +1,47 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from data.spec.schemas import IngestionRecord, RecordDetail, ErrorResponse
from backend.implementation.ingestion_history import service as ingestion_service
router = APIRouter(prefix="/api/v1", tags=["ingestion-history"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def _verify_jwt_token(token: str = Depends(oauth2_scheme)) -> str:
try:
from backend.api.auth_api import verify_jwt_token as _verify
return await _verify(token)
except HTTPException:
raise
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
@router.get(
"/ingestion-history",
response_model=list[IngestionRecord],
responses={401: {"model": ErrorResponse}},
)
async def list_ingestion_records(user_id: str = Depends(_verify_jwt_token)):
try:
return await ingestion_service.list_records(user_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
@router.get(
"/ingestion-history/{record_id}",
response_model=RecordDetail,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def get_ingestion_record(record_id: str, user_id: str = Depends(_verify_jwt_token)):
try:
return await ingestion_service.get_record(record_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))

View File

@@ -0,0 +1,59 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from data.spec.schemas import NotificationItem, NotificationPreferences, ErrorResponse
from backend.implementation.notification import service as notification_service
router = APIRouter(prefix="/api/v1", tags=["notification"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def _verify_jwt_token(token: str = Depends(oauth2_scheme)) -> str:
try:
from backend.api.auth_api import verify_jwt_token as _verify
return await _verify(token)
except HTTPException:
raise
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
@router.get(
"/notifications",
response_model=list[NotificationItem],
responses={401: {"model": ErrorResponse}},
)
async def list_notifications(user_id: str = Depends(_verify_jwt_token), filters: dict | None = None):
try:
return await notification_service.list_notifications(user_id, filters)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
@router.patch(
"/notifications/{notification_id}/read",
status_code=status.HTTP_204_NO_CONTENT,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def mark_read(notification_id: str, user_id: str = Depends(_verify_jwt_token)):
try:
await notification_service.mark_read(notification_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/notifications/preferences",
status_code=status.HTTP_204_NO_CONTENT,
responses={401: {"model": ErrorResponse}},
)
async def set_preferences(payload: NotificationPreferences, user_id: str = Depends(_verify_jwt_token)):
try:
await notification_service.set_preferences(user_id, payload.model_dump(exclude_none=True))
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))

View File

@@ -0,0 +1,91 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from data.spec.schemas import Patient, PatientCreate, PatientListResponse, ErrorResponse
from backend.implementation.patient import service as patient_service
router = APIRouter(prefix="/api/v1", tags=["patient"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def _verify_jwt_token(token: str = Depends(oauth2_scheme)) -> str:
try:
from backend.api.auth_api import verify_jwt_token as _verify
return await _verify(token)
except HTTPException:
raise
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
@router.get(
"/patients",
response_model=PatientListResponse,
responses={401: {"model": ErrorResponse}},
)
async def list_patients(user_id: str = Depends(_verify_jwt_token)):
try:
items = await patient_service.list_patients(user_id)
return PatientListResponse(items=items, total=len(items))
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
@router.post(
"/patients",
response_model=Patient,
status_code=status.HTTP_201_CREATED,
responses={401: {"model": ErrorResponse}, 422: {"model": ErrorResponse}},
)
async def create_patient(payload: PatientCreate, user_id: str = Depends(_verify_jwt_token)):
try:
return await patient_service.create_patient(payload.model_dump(exclude_none=True))
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
@router.get(
"/patients/{patient_id}",
response_model=Patient,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def get_patient(patient_id: str, user_id: str = Depends(_verify_jwt_token)):
try:
return await patient_service.get_patient(patient_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.get(
"/patients/{patient_id}/sessions",
response_model=list[dict],
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def list_patient_sessions(patient_id: str, user_id: str = Depends(_verify_jwt_token)):
try:
return await patient_service.list_sessions(patient_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.get(
"/patients/{patient_id}/history",
response_model=list[dict],
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def patient_ingestion_history(patient_id: str, user_id: str = Depends(_verify_jwt_token)):
try:
return await patient_service.ingestion_history(patient_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))

View File

@@ -0,0 +1,320 @@
import asyncio
from typing import Any
import httpx
import logging
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from fastapi.security import OAuth2PasswordBearer
from fastapi.responses import StreamingResponse
from PILOT_PROJECT.workspace.sprint_1_2.CODEBASE.data.spec.schemas.safety_schemas import ChatResponse
from data.spec.schemas import (
HeatmapResult, RationaleResult, ChatEvent, DriftCheckResult,
EvidenceList, ActivationMeta, AnnotationArtifact, EscalationTicket,
GuardrailResult, ErrorResponse, CorrectionSubmit, CorrectionRecord,
)
from backend.implementation.safety import service as safety_service
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1", tags=["safety"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def _verify_jwt_token(token: str = Depends(oauth2_scheme)) -> str:
try:
from backend.api.auth_api import verify_jwt_token as _verify
return await _verify(token)
except HTTPException:
raise
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
def _sse_chat_format(event: ChatEvent) -> str:
lines = [f"event: {event.event_type}"]
payload = event.model_dump(mode="json")
lines.append(f"data: {payload}")
lines.append("")
lines.append("")
return "\n".join(lines)
@router.post(
"/sessions/{session_id}/explanations/gradcam",
response_model=HeatmapResult,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def gradcam(session_id: str, user_id: str = Depends(_verify_jwt_token)):
try:
return await safety_service.gradcam(session_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/explanations/rationale",
response_model=RationaleResult,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def rationale(
session_id: str,
redaction_hash: str | None = None,
user_id: str = Depends(_verify_jwt_token)
):
try:
return await safety_service.rationale(session_id, redaction_hash)
except HTTPException:
raise
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/safety/circuit-breaker",
status_code=status.HTTP_204_NO_CONTENT,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def circuit_breaker(session_id: str, payload: dict, user_id: str = Depends(_verify_jwt_token)):
try:
await safety_service.circuit_break(session_id, payload.get("flag", False))
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/chat/socratic",
response_model=ChatResponse,
responses={401: {"model": ErrorResponse}, 404: {"model: ErrorResponse}},
)
async def socratic_chat(
session_id: str,
payload: dict,
redaction_hash: str | None = None,
user_id: str = Depends(_verify_jwt_token)
):
try:
return await safety_service.socratic_chat(
session_id,
payload.get("prompt", ""),
redaction_hash=redaction_hash
)
except HTTPException:
raise
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/drift/check",
response_model=DriftCheckResult,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def drift_check(session_id: str, user_id: str = Depends(_verify_jwt_token)):
try:
return await safety_service.drift_check(session_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/rag/evidence",
response_model=EvidenceList,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def rag_evidence(session_id: str, user_id: str = Depends(_verify_jwt_token)):
try:
return await safety_service.rag_evidence(session_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/activations",
response_model=ActivationMeta,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def activations(session_id: str, params: dict, user_id: str = Depends(_verify_jwt_token)):
try:
return await safety_service.activations(session_id, params)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/annotations/artifacts",
response_model=AnnotationArtifact,
status_code=status.HTTP_201_CREATED,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def upload_artifact(
session_id: str,
file: UploadFile = File(...),
user_id: str = Depends(_verify_jwt_token),
):
try:
return await safety_service.upload_artifact(session_id, file)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/ground-truth",
status_code=status.HTTP_204_NO_CONTENT,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def ground_truth(session_id: str, payload: dict, user_id: str = Depends(_verify_jwt_token)):
try:
await safety_service.ground_truth(session_id, payload.get("label", {}))
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/escalation",
response_model=EscalationTicket,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def escalate(session_id: str, payload: dict, user_id: str = Depends(_verify_jwt_token)):
try:
return await safety_service.escalate(session_id, payload.get("reason", ""))
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/annotations/morphology",
status_code=status.HTTP_204_NO_CONTENT,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def morphology(session_id: str, payload: dict, user_id: str = Depends(_verify_jwt_token)):
try:
await safety_service.morphology(session_id, payload.get("annotation", {}))
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/safety/guardrail-check",
response_model=GuardrailResult,
responses={401: {"model": ErrorResponse}, 422: {"model": ErrorResponse}},
)
async def guardrail_check(payload: dict, user_id: str = Depends(_verify_jwt_token)):
try:
session_id = payload.get("session_id", "")
return await safety_service.guardrail_check(
session_id=session_id,
prompt=payload.get("prompt", ""),
score=float(payload.get("score", 0.0)),
)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except (ValueError, TypeError) as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
@router.post(
"/sessions/{session_id}/feedback",
response_model=CorrectionRecord,
status_code=status.HTTP_201_CREATED,
responses={
401: {"model": ErrorResponse},
404: {"model": ErrorResponse},
422: {"model": ErrorResponse},
},
)
async def submit_correction(
session_id: str,
payload: CorrectionSubmit,
user_id: str = Depends(_verify_jwt_token),
):
try:
return await safety_service.submit_correction(session_id, payload.dict())
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
@router.get(
"/sessions/{session_id}/chat/stream",
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def chat_stream(
session_id: str,
prompt: str,
redaction_hash: str | None = None,
user_id: str = Depends(_verify_jwt_token),
):
async def generate():
try:
async for chunk in safety_service.chat_stream(session_id, prompt, redaction_hash):
event = ChatEvent(
session_id=session_id,
event_type="chunk",
content=chunk,
is_final=False,
)
yield _sse_chat_format(event)
final_event = ChatEvent(
session_id=session_id,
event_type="completed",
content="",
is_final=True,
)
yield _sse_chat_format(final_event)
except HTTPException as exc:
error_event = ChatEvent(
session_id=session_id,
event_type="error",
content=exc.detail,
is_final=True,
)
yield _sse_chat_format(error_event)
except NotImplementedError:
fallback_event = ChatEvent(
session_id=session_id,
event_type="error",
content="Chat streaming service not yet implemented",
is_final=True,
)
yield _sse_chat_format(fallback_event)
except asyncio.CancelledError:
logger.info(f"Chat stream cancelled for session_id={session_id}")
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
"Connection": "keep-alive",
},
)

View File

@@ -0,0 +1,214 @@
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from fastapi.security import OAuth2PasswordBearer
from data.spec.schemas import (
Session, SessionDetail, SessionCreate, SessionPatchReview,
FrameMetadata, PersistResult, ExportResult, ScrubResult,
ReportCreate, ReportSignRequest, ReportSyncEMRRequest, SyncResult,
ErrorResponse,
)
from backend.implementation.session import service as session_service
router = APIRouter(prefix="/api/v1", tags=["session"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def verify_jwt_token(token: str = Depends(oauth2_scheme)) -> str:
try:
from backend.api.auth_api import verify_jwt_token as _verify
return await _verify(token)
except HTTPException:
raise
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
@router.post(
"/sessions",
response_model=Session,
status_code=status.HTTP_201_CREATED,
responses={401: {"model": ErrorResponse}, 422: {"model": ErrorResponse}},
)
async def create_session(payload: SessionCreate, user_id: str = Depends(verify_jwt_token)):
try:
return await session_service.create_session(
user_id=user_id,
patient_id=payload.patient_id,
case_id=getattr(payload, "case_id", None),
)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
@router.get(
"/sessions/{session_id}",
response_model=SessionDetail,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def get_session(session_id: str, user_id: str = Depends(verify_jwt_token)):
try:
return await session_service.get_session(session_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/frames",
response_model=FrameMetadata,
status_code=status.HTTP_201_CREATED,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def add_frame(
session_id: str,
file: UploadFile = File(...),
frame_number: int | None = None,
user_id: str = Depends(verify_jwt_token),
):
try:
return await session_service.add_frame(session_id, file, frame_number)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.patch(
"/sessions/{session_id}/review",
response_model=Session,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def patch_review(
session_id: str,
payload: SessionPatchReview,
user_id: str = Depends(verify_jwt_token),
):
try:
return await session_service.patch_review(session_id, payload.model_dump(exclude_none=True))
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
@router.post(
"/reports",
response_model=dict,
status_code=status.HTTP_201_CREATED,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def create_report(payload: ReportCreate, user_id: str = Depends(verify_jwt_token)):
try:
result = await session_service.persist(payload.session_id, payload.payload)
return {"report_id": result.session_id, "status": result.status, "updated_at": result.updated_at.isoformat()}
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/reports/{report_id}/sign",
response_model=dict,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def sign_report(
report_id: str,
payload: ReportSignRequest,
user_id: str = Depends(verify_jwt_token),
):
try:
result = await session_service.persist(report_id, {"signed": True, "signature": payload.signature})
return {"report_id": report_id, "signed": True, "updated_at": result.updated_at.isoformat()}
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/reports/{report_id}/emr-sync",
response_model=SyncResult,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def sync_emr(
report_id: str,
payload: ReportSyncEMRRequest,
user_id: str = Depends(verify_jwt_token),
):
from datetime import datetime
try:
result = await session_service.persist(report_id, {"emr_sync": True})
return SyncResult(
report_id=report_id,
emr_status="pending",
emr_reference=None,
synced_at=datetime.utcnow(),
)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/persist",
response_model=PersistResult,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def persist_session(
session_id: str,
review: dict,
user_id: str = Depends(verify_jwt_token),
):
try:
return await session_service.persist(session_id, review)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/export-pdf",
response_model=ExportResult,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def export_pdf(
session_id: str,
params: dict,
user_id: str = Depends(verify_jwt_token),
):
try:
return await session_service.export_pdf(session_id, params)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
@router.post(
"/sessions/{session_id}/scrub-validate",
response_model=ScrubResult,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def scrub_validate(
session_id: str,
metadata: dict,
user_id: str = Depends(verify_jwt_token),
):
try:
return await session_service.scrub_validate(session_id, metadata)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))

View File

@@ -0,0 +1,47 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from data.spec.schemas import UserSettings, SettingsUpdate, ErrorResponse
from backend.implementation.settings import service as settings_service
router = APIRouter(prefix="/api/v1", tags=["settings"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def _verify_jwt_token(token: str = Depends(oauth2_scheme)) -> str:
try:
from backend.api.auth_api import verify_jwt_token as _verify
return await _verify(token)
except HTTPException:
raise
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
@router.get(
"/settings",
response_model=UserSettings,
responses={401: {"model": ErrorResponse}},
)
async def get_settings(user_id: str = Depends(_verify_jwt_token)):
try:
return await settings_service.get_settings(user_id)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
@router.patch(
"/settings",
response_model=UserSettings,
responses={401: {"model": ErrorResponse}, 422: {"model": ErrorResponse}},
)
async def update_settings(payload: SettingsUpdate, user_id: str = Depends(_verify_jwt_token)):
try:
return await settings_service.update_settings(user_id, payload.model_dump(exclude_none=True))
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))

View File

@@ -0,0 +1,40 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from data.spec.schemas import AnomalyReport, AnomalyRecord, ErrorResponse
from backend.implementation.telemetry import service as telemetry_service
router = APIRouter(prefix="/api/v1", tags=["telemetry"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def verify_jwt_token(token: str = Depends(oauth2_scheme)) -> str:
try:
from backend.api.auth_api import verify_jwt_token as _verify
return await _verify(token)
except HTTPException:
raise
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
@router.post(
"/analysis-jobs/{job_id}/anomalies",
response_model=AnomalyRecord,
status_code=status.HTTP_201_CREATED,
responses={401: {"model": ErrorResponse}, 404: {"model": ErrorResponse}},
)
async def report_anomaly(
job_id: str,
payload: AnomalyReport,
user_id: str = Depends(verify_jwt_token),
):
try:
return await telemetry_service.report_anomaly(job_id, payload.data)
except NotImplementedError as exc:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail=str(exc))
except LookupError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))