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,118 @@
import logging
from typing import Any
import httpx
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel, Field
from backend.services import agent_tools_service
from backend.services import embed_service
from data.spec.schemas import ErrorResponse
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1", tags=["agent-tools"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", auto_error=False)
class ExaSearchRequest(BaseModel):
query: str = Field(..., max_length=512)
type: str = "auto"
numResults: int = Field(default=10, ge=1, le=10)
includeDomains: list[str] | None = None
excludeDomains: list[str] | None = None
session_id: str
class SupabaseQueryRequest(BaseModel):
rpc: str
args: dict[str, Any] = Field(default_factory=dict)
session_id: str
class EmbedRequest(BaseModel):
text: str = Field(..., max_length=8192)
task: str = "retrieval-query"
title: str | None = None
async def _verify_jwt_token_optional(token: str | None = Depends(oauth2_scheme)) -> str | None:
if not token:
return None
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(
"/agent/tools/exa/search",
responses={
401: {"model": ErrorResponse},
422: {"model": ErrorResponse},
502: {"model": ErrorResponse},
},
)
async def exa_search(
body: ExaSearchRequest,
user_id: str | None = Depends(_verify_jwt_token_optional),
):
try:
return await agent_tools_service.exa_search(body.model_dump())
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
except RuntimeError as exc:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc))
except httpx.HTTPError as exc:
logger.exception("Exa upstream error")
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc))
@router.post(
"/embed",
responses={
401: {"model": ErrorResponse},
422: {"model": ErrorResponse},
},
)
async def embed(
body: EmbedRequest,
user_id: str | None = Depends(_verify_jwt_token_optional),
):
task = body.task if body.task in {"retrieval-query", "retrieval-document"} else "retrieval-query"
try:
return await embed_service.embed_text(body.text, task=task, title=body.title)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
@router.post(
"/agent/tools/supabase/query",
responses={
401: {"model": ErrorResponse},
422: {"model": ErrorResponse},
501: {"model": ErrorResponse},
502: {"model": ErrorResponse},
},
)
async def supabase_query(
body: SupabaseQueryRequest,
user_id: str | None = Depends(_verify_jwt_token_optional),
):
try:
return await agent_tools_service.supabase_query(body.model_dump())
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))
except RuntimeError as exc:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc))

View File

@@ -0,0 +1,81 @@
import logging
from fastapi import APIRouter, Depends, HTTPException, status, Body
from fastapi.responses import StreamingResponse
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
from data.spec.schemas import ErrorResponse
from backend.services.cloud_llm_gateway import route_medgemma_request
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1", tags=["cloud-consult"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
class ConsultStreamRequest(BaseModel):
session_id: str
prompt: str
task_type: str = "clinical_deep_reasoning"
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(
"/cloud-consult",
responses={401: {"model": ErrorResponse}, 403: {"model": ErrorResponse}, 502: {"model": ErrorResponse}},
)
async def cloud_consult(
payload: dict,
user_id: str = Depends(_verify_jwt_token),
):
try:
return await route_medgemma_request(payload, user_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))
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc))
@router.post(
"/cloud-consult/stream",
responses={401: {"model": ErrorResponse}, 403: {"model": ErrorResponse}},
)
async def cloud_consult_stream(
body: ConsultStreamRequest,
user_id: str = Depends(_verify_jwt_token),
):
async def generate():
async for chunk in route_medgemma_request(
{
"session_id": body.session_id,
"prompt": body.prompt,
"task_type": body.task_type,
"stream": True,
},
user_id,
):
yield chunk
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,44 @@
import logging
import httpx
from fastapi import APIRouter, Depends, HTTPException, status, Body
from fastapi.responses import StreamingResponse
from fastapi.security import OAuth2PasswordBearer
from data.spec.schemas import ErrorResponse
from backend.services.cloud_llm_gateway import route_gemini_request
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1", tags=["cloud-orchestrate"])
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(
"/cloud-orchestrate",
responses={401: {"model": ErrorResponse}, 403: {"model": ErrorResponse}, 502: {"model": ErrorResponse}},
)
async def cloud_orchestrate(
payload: dict,
user_id: str = Depends(_verify_jwt_token),
):
try:
return await route_gemini_request(payload, user_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))
except PermissionError as exc:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc))

View File

@@ -0,0 +1,223 @@
"""HTTP routes for spec-compliant CV inference (CLAHE → angle → inflammation → seg)."""
from __future__ import annotations
import io
import json
import logging
import os
import requests
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
from fastapi.responses import JSONResponse
from PIL import Image
from backend.implementation.adapters.triton_adapter import TritonAdapter
from backend.implementation.config import get_model_name, get_segmentation_model
from backend.implementation.postprocessing.calibration import CalibrationConfig, calibration_config_from_params
from backend.implementation.triton_batch import TRITON_MAX_BATCH_SIZE
from backend.services import cv_result_cache
from backend.services import triton_runtime_service as triton_runtime
from backend.services.cv_inference_service import CvBatchResult, CvInferenceOptions, run_batch, run_single
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/test", tags=["cv-inference"])
LEGACY_DEPRECATION_DETAIL = (
"This endpoint is deprecated. Use POST /api/test/analyze or POST /api/test/analyze/batch "
"for the spec-compliant CV pipeline."
)
def _is_image_upload(content_type: str | None, filename: str | None) -> bool:
if content_type and content_type.startswith("image/"):
return True
if content_type in (None, "", "application/octet-stream", "binary/octet-stream"):
name = (filename or "").lower()
return name.endswith((".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif"))
return False
def _parse_calibration_form(calibration_json: str | None) -> CalibrationConfig:
if not calibration_json:
return CalibrationConfig()
try:
data = json.loads(calibration_json)
except json.JSONDecodeError:
return CalibrationConfig()
if not isinstance(data, dict):
return CalibrationConfig()
return calibration_config_from_params({"calibration": data})
def _default_model_versions() -> dict[str, str] | None:
versions: dict[str, str] = {}
if angle := os.getenv("ANGLE_MODEL"):
versions["angle"] = angle
elif os.getenv("CV_USE_CONFIG_ANGLE_MODEL", "").lower() not in {"1", "true", "yes"}:
# Match legacy test proxy default for PoC clinical accuracy
versions["angle"] = "angle_classify_resnet50"
if inflam := os.getenv("INFLAMMATION_MODEL"):
versions["inflammation"] = inflam
if seg := os.getenv("SEGMENT_MODEL"):
versions["segmentation_sup"] = seg
versions["segmentation_post"] = seg
return versions or None
def _build_options(
calibration: CalibrationConfig,
*,
use_cache: bool = True,
) -> CvInferenceOptions:
return CvInferenceOptions(
calibration=calibration,
model_versions=_default_model_versions(),
use_cache=use_cache,
)
async def _load_upload_image(upload: UploadFile) -> Image.Image:
if not _is_image_upload(upload.content_type, upload.filename):
raise HTTPException(status_code=400, detail=f"Expected images, got {upload.filename}")
try:
raw = await upload.read()
return Image.open(io.BytesIO(raw)).convert("RGB")
except Exception as exc:
raise HTTPException(status_code=400, detail=f"Invalid image {upload.filename}: {exc}") from exc
def _triton_http_error_detail(exc: requests.HTTPError, operation: str) -> str:
status = exc.response.status_code if exc.response is not None else 503
detail = (
f"{operation} failed ({status}). "
"Modal server may be cold-starting — retry in a few seconds."
)
if exc.response is not None and exc.response.text:
detail = f"{detail} Server: {exc.response.text[:300]}"
return detail
@router.get("/health")
async def cv_inference_health():
angle_model = get_model_name("angle", _default_model_versions())
inflam_model = get_model_name("inflammation", _default_model_versions())
seg_model = get_segmentation_model("sup-up-long", _default_model_versions())
triton_endpoint = triton_runtime.get_triton_endpoint()
try:
adapter = TritonAdapter(endpoint_url=triton_endpoint, timeout=triton_runtime.TRITON_INFER_TIMEOUT)
angle_ready = await adapter.model_ready(angle_model)
inflam_ready = await adapter.model_ready(inflam_model)
seg_ready = await adapter.model_ready(seg_model)
status = "ok" if angle_ready and inflam_ready and seg_ready else "degraded"
cache = cv_result_cache.cache_stats()
return {
"status": status,
"service": "cv-inference",
"triton": triton_endpoint,
"angle_model": angle_model,
"angle_ready": angle_ready,
"inflammation_model": inflam_model,
"inflammation_ready": inflam_ready,
"segmentation_model": seg_model,
"segmentation_ready": seg_ready,
"triton_max_batch_size": TRITON_MAX_BATCH_SIZE,
"triton_infer_timeout": triton_runtime.TRITON_INFER_TIMEOUT,
"triton_infer_retries": triton_runtime.TRITON_INFER_RETRIES,
"triton_use_batch_infer": triton_runtime.TRITON_USE_BATCH_INFER,
**cache,
}
except Exception as exc:
logger.exception("CV inference health check failed")
return JSONResponse(
status_code=503,
content={"status": "error", "service": "cv-inference", "detail": str(exc), "triton": triton_endpoint},
)
@router.post("/analyze")
async def analyze_upload(
image: UploadFile = File(...),
calibration: str | None = Form(default=None),
):
"""Spec-compliant CV pipeline: CLAHE → angle → inflammation → conditional segmentation."""
image_pil = await _load_upload_image(image)
options = _build_options(_parse_calibration_form(calibration), use_cache=False)
try:
result = await run_single(image_pil, frame_id=None, options=options)
return JSONResponse(result)
except requests.HTTPError as exc:
logger.exception("Analyze pipeline failed (Triton HTTP)")
raise HTTPException(status_code=503, detail=_triton_http_error_detail(exc, "Triton analyze pipeline")) from exc
except (requests.ConnectionError, requests.Timeout) as exc:
logger.exception("Analyze pipeline failed (Triton network)")
raise HTTPException(
status_code=503,
detail=f"Triton unreachable: {exc}. Check TRITON_ENDPOINT and Modal deployment.",
) from exc
except Exception as exc:
logger.exception("Analyze pipeline failed")
raise HTTPException(status_code=500, detail=str(exc)) from exc
@router.post("/analyze/batch")
async def analyze_batch_upload(
images: list[UploadFile] = File(...),
frame_ids: str = Form(...),
calibration: str | None = Form(default=None),
):
"""Spec-compliant CV batch — one full pipeline per frame (angle-first, gated segmentation)."""
if not images:
raise HTTPException(status_code=400, detail="At least one image is required")
try:
id_list = json.loads(frame_ids)
except json.JSONDecodeError as exc:
raise HTTPException(status_code=400, detail="frame_ids must be a JSON array of strings") from exc
if not isinstance(id_list, list) or not all(isinstance(x, str) for x in id_list):
raise HTTPException(status_code=400, detail="frame_ids must be a JSON array of strings")
if len(id_list) != len(images):
raise HTTPException(status_code=400, detail="frame_ids length must match images count")
image_pils: list[Image.Image] = []
for upload in images:
image_pils.append(await _load_upload_image(upload))
options = _build_options(_parse_calibration_form(calibration))
try:
batch: CvBatchResult = await run_batch(image_pils, id_list, options=options)
cache = cv_result_cache.cache_stats()
return JSONResponse({
"success": True,
"image_count": len(batch.results),
"pipeline": "spec-cv-v1",
"triton_infer_calls": batch.triton_infer_calls,
"triton_infer_mode": batch.triton_infer_modes,
"pipeline_version": cache["pipeline_version"],
"results": batch.results,
})
except requests.HTTPError as exc:
logger.exception("Analyze batch pipeline failed (Triton HTTP)")
raise HTTPException(status_code=503, detail=_triton_http_error_detail(exc, "Triton analyze batch")) from exc
except (requests.ConnectionError, requests.Timeout) as exc:
logger.exception("Analyze batch pipeline failed (Triton network)")
raise HTTPException(
status_code=503,
detail=f"Triton unreachable: {exc}. Check TRITON_ENDPOINT and Modal deployment.",
) from exc
except Exception as exc:
logger.exception("Analyze batch pipeline failed")
raise HTTPException(status_code=500, detail=str(exc)) from exc
@router.post("/segment")
@router.post("/segment/batch")
@router.post("/angle")
@router.post("/angle/batch")
@router.post("/inflammation")
@router.post("/inflammation/batch")
async def legacy_cv_endpoints_deprecated():
raise HTTPException(status_code=410, detail=LEGACY_DEPRECATION_DETAIL)