216 lines
7.2 KiB
Python
216 lines
7.2 KiB
Python
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from app.api import rag_ask_contract
|
|
from app.api.router import api_router
|
|
from app.api.ai_steward_api import router as ai_steward_router
|
|
from app.api.rag_api import routes_query
|
|
from app.schemas.rag import RAGAskRequest
|
|
|
|
|
|
def test_rag_ask_request_normalizes_contract_fields() -> None:
|
|
assert RAGAskRequest(question="状态怎么样").thinking_mode == "deep"
|
|
req = RAGAskRequest(
|
|
question="状态怎么样",
|
|
asked_via="ai-pmo-process-text",
|
|
message_intent="MIXED",
|
|
mixed_signal_part="新增任务确认流",
|
|
action_mode="read",
|
|
)
|
|
assert req.asked_via == "/ai-pmo/process-text"
|
|
assert req.message_intent == "mixed"
|
|
assert req.mixed_signal_part == "新增任务确认流"
|
|
assert req.action_mode == "read"
|
|
|
|
|
|
def test_rag_ask_request_rejects_unknown_asked_via() -> None:
|
|
with pytest.raises(ValidationError):
|
|
RAGAskRequest(
|
|
question="hello",
|
|
asked_via="email",
|
|
)
|
|
|
|
|
|
def test_build_answer_question_kwargs_maps_fields() -> None:
|
|
request = RAGAskRequest(
|
|
question="请确认任务",
|
|
asked_via="pmflow_manual",
|
|
session_id=33,
|
|
new_session=1,
|
|
thinking_mode="deep",
|
|
action_mode="read_write",
|
|
sync_requirement=0,
|
|
message_intent="mixed",
|
|
mixed_signal_part="新增任务确认流",
|
|
)
|
|
kwargs = rag_ask_contract.build_answer_question_kwargs(
|
|
request=request,
|
|
current_user=SimpleNamespace(id="77"),
|
|
)
|
|
|
|
assert kwargs == {
|
|
"question": "请确认任务",
|
|
"asked_by": 77,
|
|
"asked_via": "pmflow_manual",
|
|
"project_id": None,
|
|
"source_chat_id": None,
|
|
"session_id": 33,
|
|
"force_new_session": True,
|
|
"thinking_mode": "deep",
|
|
"action_mode": "read_write",
|
|
"sync_requirement": False,
|
|
"message_intent": "mixed",
|
|
"mixed_signal_part": "新增任务确认流",
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rag_ask_route_forwards_contract_fields(monkeypatch) -> None:
|
|
captured: dict[str, object] = {}
|
|
|
|
class FakeKernel:
|
|
def __init__(self, db, tenant_id: int):
|
|
_ = db
|
|
captured["tenant_id"] = tenant_id
|
|
|
|
async def process_ingress(self, **kwargs):
|
|
captured["ingress_kwargs"] = kwargs
|
|
return {
|
|
"handled": True,
|
|
"status": "answered",
|
|
"intent": "query.read_only",
|
|
"direct_reply_required": True,
|
|
"direct_reply_text": "ok",
|
|
"rag_result": {"answer": "ok", "session_id": 12, "processing_stage": "qa"},
|
|
}
|
|
|
|
monkeypatch.setattr(routes_query, "AIPMOKernelService", FakeKernel)
|
|
|
|
request = RAGAskRequest(
|
|
question="现在进展怎么样",
|
|
asked_via="pmflow_manual",
|
|
action_mode="read",
|
|
message_intent="mixed",
|
|
mixed_signal_part="新增任务确认流",
|
|
)
|
|
current_user = SimpleNamespace(id=101, tenant_id=9)
|
|
response = await routes_query.rag_ask(
|
|
request=request,
|
|
current_user=current_user,
|
|
db=object(),
|
|
tenant_id=9,
|
|
)
|
|
|
|
assert response["code"] == 0
|
|
data = response["data"]
|
|
assert data["answer"] == "ok"
|
|
assert data["session_id"] == 12
|
|
ingress_kwargs = captured["ingress_kwargs"]
|
|
assert isinstance(ingress_kwargs, dict)
|
|
assert ingress_kwargs["source_system"] == "pmflow_manual"
|
|
assert ingress_kwargs["message_text"] == "现在进展怎么样"
|
|
rag_request_context = ingress_kwargs["ingress_options"]["rag_request_context"]
|
|
assert rag_request_context["asked_via"] == "pmflow_manual"
|
|
assert rag_request_context["action_mode"] == "read"
|
|
assert rag_request_context["message_intent"] == "mixed"
|
|
assert rag_request_context["mixed_signal_part"] == "新增任务确认流"
|
|
|
|
|
|
def test_ai_steward_router_does_not_expose_legacy_rag_ask_route() -> None:
|
|
route_paths = {str(getattr(route, "path", "")).strip() for route in ai_steward_router.routes}
|
|
assert "/ai-steward/rag/ask" not in route_paths
|
|
|
|
|
|
def test_api_router_uses_single_prefix_for_rag_and_ai_steward() -> None:
|
|
route_paths = {str(getattr(route, "path", "")).strip() for route in api_router.routes}
|
|
|
|
assert "/rag/ask" in route_paths
|
|
assert "/rag/rag/ask" not in route_paths
|
|
assert "/ai-steward/feedback-center/summary" in route_paths
|
|
assert "/ai-steward/ai-steward/feedback-center/summary" not in route_paths
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rag_ask_route_uses_shared_contract_builder(monkeypatch) -> None:
|
|
captured_kwargs: list[dict[str, object]] = []
|
|
captured_tenants: list[int] = []
|
|
|
|
def fake_build_answer_kwargs(request: RAGAskRequest, current_user) -> dict[str, object]:
|
|
return {
|
|
"question": request.question,
|
|
"asked_by": int(current_user.id),
|
|
"asked_via": "shared-builder",
|
|
}
|
|
|
|
monkeypatch.setattr(rag_ask_contract, "build_answer_question_kwargs", fake_build_answer_kwargs)
|
|
monkeypatch.setattr(rag_ask_contract, "resolve_effective_tenant_id", lambda *_args, **_kwargs: 88)
|
|
|
|
class FakeKernel:
|
|
def __init__(self, db, tenant_id: int):
|
|
_ = db
|
|
captured_tenants.append(int(tenant_id))
|
|
|
|
async def process_ingress(self, **kwargs):
|
|
captured_kwargs.append(dict(kwargs))
|
|
return {"handled": True, "status": "answered", "direct_reply_text": "ok", "rag_result": {"answer": "ok"}}
|
|
|
|
monkeypatch.setattr(routes_query, "AIPMOKernelService", FakeKernel)
|
|
|
|
response_rag = await routes_query.rag_ask(
|
|
request=RAGAskRequest(question="q1"),
|
|
current_user=SimpleNamespace(id=1, tenant_id=1),
|
|
db=object(),
|
|
tenant_id=1,
|
|
)
|
|
|
|
assert response_rag["code"] == 0
|
|
assert captured_tenants == [88]
|
|
assert captured_kwargs == [
|
|
{
|
|
"source_system": "pmflow",
|
|
"source_message_id": None,
|
|
"message_text": "q1",
|
|
"sender": "PMFlow用户",
|
|
"channel_ref": None,
|
|
"source_sender_open_id": None,
|
|
"captured_at": None,
|
|
"chat_type": "manual",
|
|
"project_hint_id": None,
|
|
"ingress_options": {
|
|
"rag_request_context": {
|
|
"question": "q1",
|
|
"asked_by": 1,
|
|
"asked_via": "shared-builder",
|
|
}
|
|
},
|
|
}
|
|
]
|
|
|
|
|
|
def test_build_rag_response_payload_preserves_rag_fields() -> None:
|
|
payload = rag_ask_contract.build_rag_response_payload(
|
|
ingress_result={
|
|
"status": "answered",
|
|
"intent": "query.read_only",
|
|
"work_item_id": 321,
|
|
"ai_trace_id": "ai-pmo-321",
|
|
"direct_reply_required": True,
|
|
"direct_reply_text": "fallback",
|
|
"rag_result": {
|
|
"answer": "final answer",
|
|
"session_id": 18,
|
|
"sources": [{"source": "snapshot"}],
|
|
"processing_stage": "qa",
|
|
},
|
|
}
|
|
)
|
|
|
|
assert payload["answer"] == "final answer"
|
|
assert payload["session_id"] == 18
|
|
assert payload["work_item_id"] == 321
|
|
assert payload["trace_id"] == "ai-pmo-321"
|
|
assert payload["reply_required"] is True
|
|
assert payload["reply_text"] == "fallback"
|