26-7-31-1
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""API routers."""
|
||||
@@ -0,0 +1,26 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.config import get_settings
|
||||
from app.schemas.models import HealthResponse
|
||||
from app.services.llm_client import get_llm
|
||||
from app.services.monai_preprocess import monai_available
|
||||
from app.services.rag_pipeline import get_rag, langchain_available
|
||||
from app.services.yolo_detector import yolo_available
|
||||
|
||||
router = APIRouter(tags=["health"])
|
||||
|
||||
|
||||
@router.get("/health", response_model=HealthResponse)
|
||||
def health() -> HealthResponse:
|
||||
settings = get_settings()
|
||||
rag = get_rag()
|
||||
llm = get_llm()
|
||||
return HealthResponse(
|
||||
status="ok",
|
||||
yolo_available=yolo_available(),
|
||||
monai_available=monai_available(),
|
||||
langchain_available=langchain_available(),
|
||||
llm_configured=llm.enabled,
|
||||
demo_mode=settings.demo_mode,
|
||||
knowledge_docs=rag.doc_count(),
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
|
||||
|
||||
from app.schemas.models import ImagingAnalyzeResponse
|
||||
from app.services.monai_preprocess import load_image_bgr, preprocess
|
||||
from app.services.report_generator import build_imaging_texts, generate_imaging_report, make_full_report
|
||||
from app.schemas.models import ImagingReportRequest
|
||||
from app.services.yolo_detector import get_detector
|
||||
from app.services.yolo_manager import get_yolo_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/imaging", tags=["imaging"])
|
||||
|
||||
|
||||
@router.post("/analyze", response_model=ImagingAnalyzeResponse)
|
||||
async def analyze_imaging(
|
||||
file: UploadFile | None = File(default=None),
|
||||
image_path: str | None = Form(default=None),
|
||||
study_type: str = Form(default="CT"),
|
||||
body_part: str = Form(default=""),
|
||||
patient_summary: str = Form(default=""),
|
||||
) -> ImagingAnalyzeResponse:
|
||||
raw = await _read_bytes(file, image_path)
|
||||
try:
|
||||
image_bgr = load_image_bgr(raw)
|
||||
prep = preprocess(image_bgr)
|
||||
detector = get_detector()
|
||||
detections = detector.detect(image_bgr, study_type=study_type)
|
||||
annotated = detector.annotate(image_bgr, detections)
|
||||
findings, diagnosis, recommendations, confidence = build_imaging_texts(
|
||||
study_type, body_part, detections, detector.mode
|
||||
)
|
||||
report = generate_imaging_report(
|
||||
ImagingReportRequest(
|
||||
study_type=study_type,
|
||||
body_part=body_part,
|
||||
patient_summary=patient_summary,
|
||||
preliminary_diagnosis=diagnosis,
|
||||
findings=findings,
|
||||
detections=detections,
|
||||
confidence=confidence,
|
||||
)
|
||||
)
|
||||
backend = prep.get("backend", "opencv")
|
||||
model_version = f"yolo-{detector.mode}+{backend}+{report.model_version}"
|
||||
try:
|
||||
get_yolo_manager().record_inference(
|
||||
mode=detector.mode,
|
||||
detections=detections,
|
||||
study_type=study_type,
|
||||
model_version=model_version,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("记录 YOLO 统计失败: %s", e)
|
||||
return ImagingAnalyzeResponse(
|
||||
detections=detections,
|
||||
annotated_image_base64=annotated,
|
||||
preliminary_diagnosis=report.impression or diagnosis,
|
||||
findings=report.findings or findings,
|
||||
recommendations=report.recommendations or recommendations,
|
||||
confidence=confidence,
|
||||
model_version=model_version,
|
||||
mode=detector.mode, # type: ignore[arg-type]
|
||||
full_report=report.full_report
|
||||
or make_full_report(study_type, body_part, findings, diagnosis, recommendations, patient_summary),
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
except Exception as e:
|
||||
logger.exception("影像分析失败")
|
||||
raise HTTPException(status_code=500, detail=f"影像分析失败: {e}") from e
|
||||
|
||||
|
||||
async def _read_bytes(file: UploadFile | None, image_path: str | None) -> bytes:
|
||||
if file is not None:
|
||||
data = await file.read()
|
||||
if not data:
|
||||
raise HTTPException(status_code=400, detail="上传文件为空")
|
||||
return data
|
||||
if image_path:
|
||||
path = Path(image_path)
|
||||
if not path.is_file():
|
||||
raise HTTPException(status_code=400, detail=f"影像路径不存在: {image_path}")
|
||||
return path.read_bytes()
|
||||
raise HTTPException(status_code=400, detail="请提供 file 或 image_path")
|
||||
@@ -0,0 +1,70 @@
|
||||
"""管理端下发的 LLM 运行时配置。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.config import get_settings
|
||||
from app.services.llm_client import get_llm
|
||||
from app.services.llm_runtime import get_runtime_llm, update_runtime_llm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/llm", tags=["llm"])
|
||||
|
||||
|
||||
class LlmConfigUpdate(BaseModel):
|
||||
enabled: bool | None = True
|
||||
api_base_url: str | None = Field(default=None, alias="api_base_url")
|
||||
api_key: str | None = None
|
||||
model: str | None = None
|
||||
temperature: float | None = None
|
||||
|
||||
# 兼容 camelCase(Spring 默认可能发 apiBaseUrl)
|
||||
apiBaseUrl: str | None = None
|
||||
apiKey: str | None = None
|
||||
|
||||
class Config:
|
||||
populate_by_name = True
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
def get_llm_config() -> dict:
|
||||
return get_llm().info()
|
||||
|
||||
|
||||
@router.post("/config")
|
||||
def set_llm_config(body: LlmConfigUpdate) -> dict:
|
||||
base = body.api_base_url or body.apiBaseUrl
|
||||
key = body.api_key if body.api_key is not None else body.apiKey
|
||||
# 注意:key 为 None 表示本次不改密钥;空字符串表示清空运行时密钥
|
||||
rt = update_runtime_llm(
|
||||
enabled=body.enabled,
|
||||
api_base_url=base,
|
||||
api_key=key,
|
||||
model=body.model,
|
||||
temperature=body.temperature,
|
||||
)
|
||||
info = get_llm().info()
|
||||
logger.info(
|
||||
"已更新运行时 LLM 配置 enabled=%s model=%s base=%s key=%s source=%s",
|
||||
info.get("enabled"),
|
||||
info.get("model"),
|
||||
info.get("api_base_url"),
|
||||
"yes" if info.get("api_key_configured") else "no",
|
||||
rt.source,
|
||||
)
|
||||
return {"ok": True, **info}
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
def llm_status() -> dict:
|
||||
settings = get_settings()
|
||||
info = get_llm().info()
|
||||
rt = get_runtime_llm()
|
||||
return {
|
||||
**info,
|
||||
"env_key_configured": bool(settings.llm_api_key and settings.llm_api_key.strip()),
|
||||
"runtime_enabled_flag": rt.enabled,
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.schemas.models import RagQueryRequest, RagQueryResponse, SourceRef
|
||||
from app.services.rag_pipeline import get_rag
|
||||
|
||||
router = APIRouter(prefix="/rag", tags=["rag"])
|
||||
|
||||
|
||||
class IngestRequest(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
category: str = "自定义"
|
||||
|
||||
|
||||
class IngestResponse(BaseModel):
|
||||
ok: bool = True
|
||||
chunks: int = 0
|
||||
|
||||
|
||||
@router.post("/query", response_model=RagQueryResponse)
|
||||
def rag_query(body: RagQueryRequest) -> RagQueryResponse:
|
||||
rag = get_rag()
|
||||
result = rag.query(body.query, top_k=body.top_k, extra_context=body.context or "")
|
||||
raw_sources = result.get("sources") or []
|
||||
sources: list[SourceRef] = []
|
||||
for s in raw_sources:
|
||||
if isinstance(s, SourceRef):
|
||||
sources.append(s)
|
||||
elif isinstance(s, dict):
|
||||
sources.append(SourceRef(**s))
|
||||
return RagQueryResponse(
|
||||
answer=result.get("answer") or "",
|
||||
sources=sources,
|
||||
engine=result.get("engine") or "langchain-rag",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/ingest", response_model=IngestResponse)
|
||||
def rag_ingest(body: IngestRequest) -> IngestResponse:
|
||||
rag = get_rag()
|
||||
rag.ingest_text(body.title, body.content, body.category)
|
||||
return IngestResponse(ok=True, chunks=rag.doc_count())
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
def rag_stats() -> dict:
|
||||
rag = get_rag()
|
||||
return {"chunks": rag.doc_count()}
|
||||
@@ -0,0 +1,21 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.schemas.models import (
|
||||
DecisionRequest,
|
||||
DecisionResponse,
|
||||
ImagingReportRequest,
|
||||
ImagingReportResponse,
|
||||
)
|
||||
from app.services.report_generator import generate_decision, generate_imaging_report
|
||||
|
||||
router = APIRouter(prefix="/report", tags=["report"])
|
||||
|
||||
|
||||
@router.post("/imaging", response_model=ImagingReportResponse)
|
||||
def report_imaging(body: ImagingReportRequest) -> ImagingReportResponse:
|
||||
return generate_imaging_report(body)
|
||||
|
||||
|
||||
@router.post("/decision", response_model=DecisionResponse)
|
||||
def report_decision(body: DecisionRequest) -> DecisionResponse:
|
||||
return generate_decision(body)
|
||||
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, File, HTTPException, UploadFile
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.services.yolo_detector import get_detector, reload_detector, yolo_available
|
||||
from app.services.yolo_manager import get_yolo_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/yolo", tags=["yolo"])
|
||||
|
||||
|
||||
class ActivateRequest(BaseModel):
|
||||
name: str = Field(..., description="权重文件名")
|
||||
demo_mode: str | None = Field(default=None, description="demo|real|auto")
|
||||
|
||||
|
||||
class DemoModeRequest(BaseModel):
|
||||
demo_mode: str = Field(..., description="demo|real|auto")
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
def yolo_status() -> dict:
|
||||
mgr = get_yolo_manager()
|
||||
det = get_detector()
|
||||
return mgr.status(det.info())
|
||||
|
||||
|
||||
@router.get("/weights")
|
||||
def list_weights() -> dict:
|
||||
mgr = get_yolo_manager()
|
||||
return {"items": mgr.list_weights(), "count": len(mgr.list_weights())}
|
||||
|
||||
|
||||
@router.post("/weights/upload")
|
||||
async def upload_weight(file: UploadFile = File(...)) -> dict:
|
||||
if not file.filename:
|
||||
raise HTTPException(400, "缺少文件名")
|
||||
raw = await file.read()
|
||||
try:
|
||||
item = get_yolo_manager().save_upload(file.filename, raw)
|
||||
return {"ok": True, "weight": item}
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e)) from e
|
||||
except Exception as e:
|
||||
logger.exception("上传权重失败")
|
||||
raise HTTPException(500, f"上传失败: {e}") from e
|
||||
|
||||
|
||||
@router.post("/weights/activate")
|
||||
def activate_weight(body: ActivateRequest) -> dict:
|
||||
try:
|
||||
status = get_yolo_manager().activate(body.name, body.demo_mode)
|
||||
info = reload_detector()
|
||||
status.update(info)
|
||||
status["ok"] = True
|
||||
return status
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(404, str(e)) from e
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e)) from e
|
||||
|
||||
|
||||
@router.post("/weights/deactivate")
|
||||
def deactivate_weight() -> dict:
|
||||
status = get_yolo_manager().deactivate()
|
||||
info = reload_detector()
|
||||
status.update(info)
|
||||
status["ok"] = True
|
||||
return status
|
||||
|
||||
|
||||
@router.delete("/weights/{name}")
|
||||
def delete_weight(name: str) -> dict:
|
||||
"""逻辑删除:隐藏权重,本地 .pt 文件仍保留在 data/weights。"""
|
||||
try:
|
||||
get_yolo_manager().delete_weight(name)
|
||||
reload_detector()
|
||||
return {
|
||||
"ok": True,
|
||||
"name": name,
|
||||
"soft_delete": True,
|
||||
"message": "已逻辑删除(本地文件保留,仅从列表隐藏)",
|
||||
}
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(404, str(e)) from e
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e)) from e
|
||||
|
||||
|
||||
@router.post("/weights/restore")
|
||||
def restore_weight(body: ActivateRequest) -> dict:
|
||||
"""恢复逻辑删除的权重。"""
|
||||
try:
|
||||
status = get_yolo_manager().restore_weight(body.name)
|
||||
info = reload_detector()
|
||||
status.update(info)
|
||||
status["ok"] = True
|
||||
return status
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(404, str(e)) from e
|
||||
|
||||
|
||||
@router.post("/mode")
|
||||
def set_mode(body: DemoModeRequest) -> dict:
|
||||
try:
|
||||
status = get_yolo_manager().set_demo_mode(body.demo_mode)
|
||||
info = reload_detector()
|
||||
status.update(info)
|
||||
status["ok"] = True
|
||||
return status
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e)) from e
|
||||
|
||||
|
||||
@router.get("/visualization")
|
||||
def visualization() -> dict:
|
||||
return get_yolo_manager().visualization()
|
||||
|
||||
|
||||
@router.post("/stats/reset")
|
||||
def reset_stats() -> dict:
|
||||
get_yolo_manager().reset_stats()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/capability")
|
||||
def capability() -> dict:
|
||||
return {
|
||||
"ultralytics": yolo_available(),
|
||||
"detector": get_detector().info(),
|
||||
"manager": get_yolo_manager().status(),
|
||||
}
|
||||
Reference in New Issue
Block a user