- private_docs.py: 命中/未命中两态响应包 ok:true(错误信封 ok:false 不变)
- schemas.py: PrivateQARequest.question 加 min_length=1
- privateqa.py: LLM 自谦答语(资料中未找到相关信息+等价变体)确定性改判
{hit:false,mode:empty},不调默认阈值(底噪 0.3-0.37 与真实命中 0.42-0.73
有重叠)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
65 lines
2.4 KiB
Python
65 lines
2.4 KiB
Python
"""routers/private_docs.py —— 私有资料问答 BFF 端点(FR-PRIV-004,A1)。
|
||
|
||
- POST /api/qa/private body={question, session_id?, top_k?, min_score?}
|
||
|
||
三态契约(FR-PRIV 假设 8):
|
||
- 命中:200 {ok:true, hit:true, mode:"vector", answer, items, elapsed_ms}
|
||
- 未命中:200 {ok:true, hit:false, mode:"empty", answer:"", items:[]}
|
||
- vstore 不可达:503 UPSTREAM_DOWN;LLM 失败:502 UPSTREAM_ERROR
|
||
(信封复用 app.upstream.envelope:{ok:false, error:{code, message, degraded}})
|
||
|
||
技术纪律:无生成器/无 SSE(Cython 安全);服务层只用 dict/stdlib。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Any
|
||
|
||
from fastapi import APIRouter
|
||
from fastapi.responses import JSONResponse
|
||
|
||
from ..schemas import PrivateQARequest
|
||
from ..services.docgen.client import DocgenUnavailable
|
||
from ..services.docgen.llm import LLMError
|
||
from ..services.privateqa import ask_private
|
||
from ..upstream import envelope
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter(prefix="/api/qa", tags=["private_docs"])
|
||
|
||
|
||
def _envelope(
|
||
code: str,
|
||
message: str | None = None,
|
||
degraded: bool | None = None,
|
||
status: int | None = None,
|
||
) -> JSONResponse:
|
||
if degraded is None:
|
||
degraded = code == "UPSTREAM_DOWN"
|
||
body, http = envelope(code, message=message, degraded=degraded, status=status)
|
||
return JSONResponse(status_code=http, content=body)
|
||
|
||
|
||
@router.post("/private")
|
||
async def private_qa(req: PrivateQARequest) -> dict[str, Any]:
|
||
"""私有资料问答:命中/未命中 200(均带 ok:true 信封,BUG-PRIV-R1-01);
|
||
上游故障转错误信封(ok:false)。"""
|
||
try:
|
||
result = await ask_private(
|
||
req.question,
|
||
top_k=req.top_k,
|
||
min_score=req.min_score,
|
||
)
|
||
# BUG-PRIV-R1-01(TL 裁决):命中与未命中两态响应均包 ok:true
|
||
return {"ok": True, **result}
|
||
except DocgenUnavailable as exc:
|
||
logger.warning("privateqa: vstore 不可达:%s", exc)
|
||
return _envelope("UPSTREAM_DOWN", message="私有资料检索服务不可用,请稍后重试")
|
||
except LLMError as exc:
|
||
logger.warning("privateqa: LLM 失败:%s", exc)
|
||
return _envelope("UPSTREAM_ERROR", message="私有资料回答生成失败,请稍后重试")
|
||
except Exception as exc:
|
||
logger.exception("privateqa: 未预期异常")
|
||
return _envelope("INTERNAL", message=f"私有资料问答失败:{exc}")
|