26-7-31-1

This commit is contained in:
shuai
2026-07-31 12:27:41 +08:00
commit 9b8b8b57b3
142 changed files with 22408 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""API routers."""
+26
View File
@@ -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(),
)
+89
View File
@@ -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")
+70
View File
@@ -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,
}
+49
View File
@@ -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()}
+21
View File
@@ -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)
+135
View File
@@ -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(),
}