diff --git a/backend/app/api/routes/chat.py b/backend/app/api/routes/chat.py index 1d495c8..accd0e7 100644 --- a/backend/app/api/routes/chat.py +++ b/backend/app/api/routes/chat.py @@ -1,15 +1,34 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from app.db.session import get_db from app.schemas.chat import ChatRequest, ChatResponse +from app.schemas.retrieval import RetrievalRequest +from app.services.retrieval import retrieve router = APIRouter(tags=["chat"]) @router.post("/chat", response_model=ChatResponse) -def chat(payload: ChatRequest) -> ChatResponse: - # TODO: retrieval -> context assembly -> LLM -> evidence/source checks +def chat(payload: ChatRequest, db: Session = Depends(get_db)) -> ChatResponse: + """运行可复核的关键词检索;模型未配置时绝不伪造生成式回答。""" + result = retrieve(db, RetrievalRequest(query=payload.question, top_k=5)) + if result.status == "no_evidence": + return ChatResponse( + answer="未检索到可用的已核验知识依据。请补充更具体的关键词,或由管理员补充并审核资料。", + status="no_evidence", + sources=[], + retrieval_run_id=result.run_id, + ) return ChatResponse( - answer=f"已收到问题:{payload.question}。当前为基础壳子,尚未接入知识检索或大语言模型。", - status="stub", - sources=[], + answer=( + f"已检索到 {result.total_hits} 条已核验知识依据。当前尚未配置大语言模型," + "因此系统仅返回可追溯证据,不生成或改写答案。" + ), + status="retrieval_only", + sources=[ + {"title": hit.title, "source_url": hit.source_url} + for hit in result.results + ], + retrieval_run_id=result.run_id, ) diff --git a/backend/app/schemas/chat.py b/backend/app/schemas/chat.py index 3880934..ceb838a 100644 --- a/backend/app/schemas/chat.py +++ b/backend/app/schemas/chat.py @@ -1,3 +1,6 @@ +from typing import Literal +from uuid import UUID + from pydantic import BaseModel, Field @@ -12,5 +15,6 @@ class SourcePreview(BaseModel): class ChatResponse(BaseModel): answer: str - status: str + status: Literal["retrieval_only", "no_evidence"] sources: list[SourcePreview] = [] + retrieval_run_id: UUID diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index e3cf403..03ccbfa 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -271,7 +271,9 @@ def test_all_management_routes_require_admin_and_history_is_private(anonymous_cl for method, path in paths: assert c.request(method, path, json={}).status_code == 403, path assert c.post("/api/retrieval", json={"query": "占位"}).json()["status"] == "no_evidence" - assert c.post("/api/chat", json={"question": "占位"}).json()["status"] == "stub" + response = c.post("/api/chat", json={"question": "占位"}) + assert response.status_code == 200 + assert response.json()["status"] == "no_evidence" def test_admin_access_live_role_and_no_csrf_bypass(client, migrated_database): diff --git a/backend/tests/test_catalog.py b/backend/tests/test_catalog.py index dc400dc..0be81c6 100644 --- a/backend/tests/test_catalog.py +++ b/backend/tests/test_catalog.py @@ -59,12 +59,28 @@ def review(client, item, status="verified"): ) -def test_health_docs_and_stub(client): +def test_health_docs_and_chat_without_evidence(client): assert client.get("/api/health").json()["status"] == "ok" assert client.get("/api/ready").json()["database"] == "connected" assert client.get("/docs").status_code == 200 assert "/api/knowledge/{item_id}/review" in client.get("/openapi.json").json()["paths"] - assert client.post("/api/chat", json={"question": "测试"}).json()["status"] == "stub" + response = client.post("/api/chat", json={"question": "测试"}) + assert response.status_code == 200 + assert response.json()["status"] == "no_evidence" + + +def test_chat_returns_real_retrieval_evidence_without_generation(client): + item = knowledge(client, title="那拉提测试资料", content="那拉提文旅服务测试依据") + assert review(client, item).status_code == 200 + + response = client.post("/api/chat", json={"question": "那拉提文旅服务"}) + + assert response.status_code == 200 + body = response.json() + assert body["status"] == "retrieval_only" + assert "不生成" in body["answer"] + assert body["retrieval_run_id"] + assert body["sources"] == [{"title": "那拉提测试资料", "source_url": "https://example.com/"}] def test_legacy_create_and_missing_evidence(client): diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 49fe087..a00280f 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -3,7 +3,7 @@ import type { VectorRetrievalResult } from './services/vectors' import type { ChunkKeywordResult, HybridResult } from './services/hybrid' export const getHealth = () => apiRequest<{status:string}>('/api/health') -export const askQuestion = (question:string) => apiRequest<{answer:string;status:string;sources:{title:string;source_url?:string|null}[]}>('/api/chat',{method:'POST',body:JSON.stringify({question})}) +export const askQuestion = (question:string) => apiRequest<{answer:string;status:'retrieval_only'|'no_evidence';sources:{title:string;source_url?:string|null}[];retrieval_run_id:string}>('/api/chat',{method:'POST',body:JSON.stringify({question})}) export const getKnowledge = () => apiRequest<{id:number;title:string;category:string;content:string;source_url:string|null;verification_status:string;version:number}[]>('/api/knowledge') export const createKnowledge = (payload:{title:string;category:string;content:string;source_url?:string;collected_at?:string}) => apiRequest('/api/knowledge',{method:'POST',body:JSON.stringify(payload)},true)