update the codebase poc ver1
This commit is contained in:
118
workspace/sprint_1_2/CODEBASE/backend/routers/agent_tools.py
Normal file
118
workspace/sprint_1_2/CODEBASE/backend/routers/agent_tools.py
Normal 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))
|
||||
@@ -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",
|
||||
},
|
||||
)
|
||||
@@ -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))
|
||||
223
workspace/sprint_1_2/CODEBASE/backend/routers/cv_inference.py
Normal file
223
workspace/sprint_1_2/CODEBASE/backend/routers/cv_inference.py
Normal 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)
|
||||
Reference in New Issue
Block a user