5219 lines
194 KiB
Python
5219 lines
194 KiB
Python
import pytest
|
||
import time
|
||
from types import SimpleNamespace
|
||
|
||
from app.config.ai_prompts import PROMPT_AI_PMO_QA, PROMPT_RAG_QUERY_SEMANTICS
|
||
from app.services.adaptive_rag_service.mixin_answer_entry import AdaptiveRAGAnswerEntryMixin
|
||
from app.services.adaptive_rag_service.mixin_answer_mainline import AdaptiveRAGAnswerMainlineMixin
|
||
from app.services.adaptive_rag_service.mixin_answer_payload import AdaptiveRAGAnswerPayloadMixin
|
||
from app.services.adaptive_rag_service.mixin_answer_precheck import AdaptiveRAGAnswerPrecheckMixin
|
||
from app.services.adaptive_rag_service.mixin_answer_retrieval import AdaptiveRAGAnswerRetrievalMixin
|
||
from app.services.adaptive_rag_service.mixin_conversation_utils_a_p1 import AdaptiveRAGConversationUtilsAMixinP1
|
||
from app.services.adaptive_rag_service.mixin_deep_guard_answer import AdaptiveRAGDeepGuardAnswerMixin
|
||
from app.services.adaptive_rag_service.mixin_deep_guard_rules import AdaptiveRAGDeepGuardRulesMixin
|
||
from app.services.adaptive_rag_service.mixin_core_intent import AdaptiveRAGCoreIntentMixin
|
||
from app.services.adaptive_rag_service.mixin_deterministic_query_core import (
|
||
AdaptiveRAGDeterministicQueryCoreMixin,
|
||
)
|
||
from app.services.adaptive_rag_service.mixin_manifest_feature_p2 import AdaptiveRAGManifestFeatureMixinP2
|
||
from app.services.adaptive_rag_service.mixin_manifest_feature_p1 import AdaptiveRAGManifestFeatureMixinP1
|
||
from app.services.adaptive_rag_service.mixin_multistep_core import AdaptiveRAGMultiStepCoreMixin
|
||
from app.services.adaptive_rag_service.mixin_multistep_flow import AdaptiveRAGMultiStepFlowMixin
|
||
from app.services.adaptive_rag_service.mixin_query_route_gateway import AdaptiveRAGQueryRouteGatewayMixin
|
||
from app.services.adaptive_rag_service.mixin_requirement_ack_core import AdaptiveRAGRequirementAckCoreMixin
|
||
from app.services.adaptive_rag_service.mixin_retrieval_router_db import AdaptiveRAGRetrievalRouterDBMixin
|
||
from app.services.adaptive_rag_service.mixin_retrieval_vector import AdaptiveRAGRetrievalVectorMixin
|
||
from app.services.adaptive_rag_service.mixin_followup_anchor import FollowupAnchorResolution
|
||
from app.services.adaptive_rag_service.mixin_followup_anchor import AdaptiveRAGFollowupAnchorMixin
|
||
from app.services.adaptive_rag_service.mixin_fast_answer_project_main import AdaptiveRAGFastAnswerProjectMainMixin
|
||
from app.services.adaptive_rag_service.mixin_fast_answer_project_utils import AdaptiveRAGFastAnswerProjectUtilsMixin
|
||
from app.services.adaptive_rag_service.internal_query_context import InternalContextPacket
|
||
from app.services.adaptive_rag_service.mixin_target_resolution import AdaptiveRAGTargetResolutionMixin
|
||
from app.services.adaptive_rag_service.shared_part_02 import RetrievedItem
|
||
from app.services.adaptive_rag_service.shared_part_02 import ACTION_MODE_READ, DEEP_RETRIEVAL_STEPS, DEEP_VECTOR_TOP_K
|
||
from app.services.ai_pmo.workflow_semantic_planner import WorkflowSemanticPlanner
|
||
|
||
|
||
def _make_internal_context_packet(
|
||
*,
|
||
query_type: str,
|
||
answer_contract: str,
|
||
target_ref: str | None = None,
|
||
) -> InternalContextPacket:
|
||
target_resolution = {}
|
||
if target_ref:
|
||
target_resolution = {
|
||
"target_entity_type": "project",
|
||
"target_ref": target_ref,
|
||
"resolution_mode": "deterministic",
|
||
"confidence": 0.96,
|
||
}
|
||
return InternalContextPacket.model_validate(
|
||
{
|
||
"packet_version": "v1",
|
||
"question": "",
|
||
"scope": {
|
||
"tenant_id": 1,
|
||
"project_id": None,
|
||
"session_id": 11,
|
||
"conversation_scope_key": None,
|
||
"reply_to_message_id": None,
|
||
"root_message_id": None,
|
||
},
|
||
"route_decision": {
|
||
"route_mode": "internal",
|
||
"effective_source_policy": "internal",
|
||
},
|
||
"operation_contract": {
|
||
"query_type": query_type,
|
||
},
|
||
"target_resolution": target_resolution,
|
||
"query_plan": {
|
||
"plan_version": "v2",
|
||
"query_type": query_type,
|
||
"entity_type": "project",
|
||
"answer_contract": answer_contract,
|
||
"target_ref": target_ref,
|
||
"filters": [],
|
||
},
|
||
"executor_summary": {},
|
||
"verifier_summary": {},
|
||
"uncertainties": [],
|
||
}
|
||
)
|
||
|
||
|
||
class _FakeAdaptiveRAGService(
|
||
AdaptiveRAGAnswerEntryMixin,
|
||
AdaptiveRAGAnswerMainlineMixin,
|
||
):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 9
|
||
self.ack_calls = 0
|
||
self.base_calls = 0
|
||
self.base_kwargs: dict[str, object] = {}
|
||
|
||
def _normalize_thinking_mode(self, thinking_mode: str) -> str:
|
||
return "deep" if str(thinking_mode or "").strip().lower() == "deep" else "normal"
|
||
|
||
async def _prepare_answer_runtime(self, **kwargs):
|
||
clean_q = str(kwargs.get("clean_q") or "").strip()
|
||
action_mode = str(kwargs.get("action_mode") or "").strip().lower()
|
||
return {
|
||
"session_id": 101,
|
||
"turn_index": 2,
|
||
"context_session_id": 101,
|
||
"conversation": [],
|
||
"state_row": None,
|
||
"resolved_question": clean_q,
|
||
"effective_q": clean_q,
|
||
"rewrite_meta": {"mode": "direct"},
|
||
"query_type": "explain",
|
||
"effective_action_mode": action_mode,
|
||
"anchor_resolution": None,
|
||
}
|
||
|
||
async def _handle_ack_first_requirement(self, **kwargs):
|
||
self.ack_calls += 1
|
||
_ = kwargs
|
||
return {"answer": "ack-required", "processing_stage": "ack_only"}
|
||
|
||
async def _execute_base_answer_mainline(self, **kwargs):
|
||
self.base_calls += 1
|
||
self.base_kwargs = dict(kwargs)
|
||
return {"answer": "base-answer", "processing_stage": "qa"}
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_answer_entry_read_mainline_skips_requirement_ack() -> None:
|
||
service = _FakeAdaptiveRAGService()
|
||
|
||
result = await service.answer_question(
|
||
question="现在有哪些阻塞?",
|
||
asked_by=1,
|
||
asked_via="pmflow_manual",
|
||
action_mode="read",
|
||
sync_requirement=True,
|
||
)
|
||
|
||
assert result["processing_stage"] == "qa"
|
||
assert service.ack_calls == 0
|
||
assert service.base_calls == 1
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_answer_entry_write_mode_uses_requirement_ack_gate() -> None:
|
||
service = _FakeAdaptiveRAGService()
|
||
|
||
result = await service.answer_question(
|
||
question="请同步写入一条风险",
|
||
asked_by=1,
|
||
asked_via="pmflow_manual",
|
||
action_mode="write",
|
||
sync_requirement=True,
|
||
)
|
||
|
||
assert result["processing_stage"] == "ack_only"
|
||
assert service.ack_calls == 1
|
||
assert service.base_calls == 0
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_answer_entry_high_accuracy_channel_respects_requested_normal_mode() -> None:
|
||
service = _FakeAdaptiveRAGService()
|
||
|
||
result = await service.answer_question(
|
||
question="最近有哪些项目?",
|
||
asked_by=1,
|
||
asked_via="pmflow",
|
||
thinking_mode="normal",
|
||
sync_requirement=True,
|
||
)
|
||
|
||
assert result["processing_stage"] == "qa"
|
||
assert service.ack_calls == 0
|
||
assert service.base_calls == 1
|
||
assert service.base_kwargs["active_thinking_mode"] == "normal"
|
||
assert service.base_kwargs["deep_thinking"] is False
|
||
assert int(service.base_kwargs["deep_retrieval_steps"]) == DEEP_RETRIEVAL_STEPS
|
||
assert int(service.base_kwargs["deep_vector_top_k"]) == DEEP_VECTOR_TOP_K
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_answer_entry_forwards_consultant_mode_preference_to_mainline() -> None:
|
||
service = _FakeAdaptiveRAGService()
|
||
|
||
result = await service.answer_question(
|
||
question="这个方案怎么做更稳妥?",
|
||
asked_by=1,
|
||
asked_via="pmflow",
|
||
thinking_mode="normal",
|
||
consultant_mode_preference="advise",
|
||
)
|
||
|
||
assert result["processing_stage"] == "qa"
|
||
assert service.base_calls == 1
|
||
assert service.base_kwargs["consultant_mode_preference"] == "advise"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_answer_entry_mixed_request_skips_requirement_ack_gate() -> None:
|
||
service = _FakeAdaptiveRAGService()
|
||
|
||
result = await service.answer_question(
|
||
question="现在进展如何,并且新增任务确认流",
|
||
asked_by=1,
|
||
asked_via="pmflow_manual",
|
||
action_mode="read_write",
|
||
sync_requirement=True,
|
||
message_intent="mixed",
|
||
)
|
||
|
||
assert result["processing_stage"] == "qa"
|
||
assert service.ack_calls == 0
|
||
assert service.base_calls == 1
|
||
assert service.base_kwargs["active_thinking_mode"] == "normal"
|
||
assert service.base_kwargs["deep_thinking"] is False
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_answer_entry_feishu_write_mode_still_goes_base_mainline() -> None:
|
||
service = _FakeAdaptiveRAGService()
|
||
|
||
result = await service.answer_question(
|
||
question="请在飞书里补一条风险",
|
||
asked_by=1,
|
||
asked_via="feishu",
|
||
action_mode="write",
|
||
sync_requirement=True,
|
||
)
|
||
|
||
assert result["processing_stage"] == "qa"
|
||
assert service.ack_calls == 0
|
||
assert service.base_calls == 1
|
||
assert service.base_kwargs["active_thinking_mode"] == "normal"
|
||
assert service.base_kwargs["deep_thinking"] is False
|
||
assert int(service.base_kwargs["deep_retrieval_steps"]) == DEEP_RETRIEVAL_STEPS
|
||
assert int(service.base_kwargs["deep_vector_top_k"]) == DEEP_VECTOR_TOP_K
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_requirement_ack_auto_mode_respects_structured_query_bundle(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
class _ModeService(AdaptiveRAGRequirementAckCoreMixin):
|
||
pass
|
||
|
||
async def _fake_aplan_message(cls, *, message_text: str, source_system: str, metadata=None):
|
||
_ = (cls, message_text, source_system, metadata)
|
||
return WorkflowSemanticPlanner.deserialize_bundle(
|
||
{
|
||
"bundle_id": "semantic-test",
|
||
"semantic_version": "v1",
|
||
"source_system": "adaptive_rag_ack",
|
||
"primary_workflow": "query_flow",
|
||
"workflows": [
|
||
{
|
||
"workflow_type": "query_flow",
|
||
"workflow_action": "answer",
|
||
"target_scope": "global",
|
||
"operation_codes": ["query.answer"],
|
||
"requires_confirmation": False,
|
||
"confidence": 0.91,
|
||
"clarification_needed": False,
|
||
"metadata": {},
|
||
}
|
||
],
|
||
"message_gate": "auto",
|
||
"reply_contract": {"mode": "text", "template_key": "workflow_bundle", "summary": "query_flow"},
|
||
"diagnostics": {"message_intent": "question"},
|
||
}
|
||
)
|
||
|
||
monkeypatch.setattr(WorkflowSemanticPlanner, "aplan_message", classmethod(_fake_aplan_message))
|
||
|
||
mode = await _ModeService._resolve_action_mode_async(
|
||
requested_mode="auto",
|
||
question="先不要创建任务,告诉我当前进展和风险",
|
||
effective_question="先不要创建任务,告诉我当前进展和风险",
|
||
asked_via="pmflow_manual",
|
||
)
|
||
|
||
assert mode == ACTION_MODE_READ
|
||
|
||
|
||
def test_query_runtime_planner_prefers_fast_path_for_list_question_even_if_requested_deep() -> None:
|
||
class _FakePlannerService(AdaptiveRAGAnswerMainlineMixin):
|
||
pass
|
||
|
||
service = _FakePlannerService()
|
||
decision = service._plan_query_runtime(
|
||
question="现在有哪些项目?",
|
||
effective_question="现在有哪些项目?",
|
||
query_type="list",
|
||
requested_thinking_mode="deep",
|
||
enforce_scene_prompt_only=False,
|
||
)
|
||
|
||
assert decision.answer_mode == "fast_path"
|
||
assert decision.thinking_mode == "normal"
|
||
assert decision.allow_fast_path is True
|
||
|
||
|
||
def test_query_runtime_planner_bypasses_fast_path_when_consultant_mode_preference_present() -> None:
|
||
class _FakePlannerService(AdaptiveRAGAnswerMainlineMixin):
|
||
pass
|
||
|
||
service = _FakePlannerService()
|
||
decision = service._plan_query_runtime(
|
||
question="现在有哪些项目?",
|
||
effective_question="现在有哪些项目?",
|
||
query_type="list",
|
||
requested_thinking_mode="normal",
|
||
enforce_scene_prompt_only=False,
|
||
consultant_mode_preference="advise",
|
||
)
|
||
|
||
assert decision.answer_mode == "retrieval_normal"
|
||
assert decision.thinking_mode == "normal"
|
||
assert decision.allow_fast_path is False
|
||
|
||
|
||
def test_query_runtime_planner_ignores_legacy_consultant_mode_for_internal_existence_query() -> None:
|
||
class _FakePlannerService(AdaptiveRAGAnswerMainlineMixin):
|
||
pass
|
||
|
||
service = _FakePlannerService()
|
||
decision = service._plan_query_runtime(
|
||
question="当前系统内是否有为采购经理提出的需求?",
|
||
effective_question="当前系统内是否有为采购经理提出的需求?",
|
||
query_type="existence",
|
||
requested_thinking_mode="normal",
|
||
enforce_scene_prompt_only=False,
|
||
consultant_mode_preference="analytical",
|
||
)
|
||
|
||
assert decision.answer_mode == "fast_path"
|
||
assert decision.thinking_mode == "normal"
|
||
assert decision.allow_fast_path is True
|
||
|
||
|
||
def test_query_runtime_planner_prefers_fast_path_for_existence_question() -> None:
|
||
class _FakePlannerService(AdaptiveRAGAnswerMainlineMixin):
|
||
pass
|
||
|
||
service = _FakePlannerService()
|
||
decision = service._plan_query_runtime(
|
||
question="系统里有没有A股项目?",
|
||
effective_question="系统里有没有A股项目?",
|
||
query_type="existence",
|
||
requested_thinking_mode="normal",
|
||
enforce_scene_prompt_only=False,
|
||
)
|
||
|
||
assert decision.answer_mode == "fast_path"
|
||
assert decision.thinking_mode == "normal"
|
||
assert decision.allow_fast_path is True
|
||
|
||
|
||
def test_internal_query_plan_builds_project_existence_contract() -> None:
|
||
class _FakePlannerService(
|
||
AdaptiveRAGAnswerMainlineMixin,
|
||
AdaptiveRAGFastAnswerProjectUtilsMixin,
|
||
):
|
||
pass
|
||
|
||
service = _FakePlannerService()
|
||
plan = service._build_internal_query_plan(
|
||
question="系统里有没有A股项目?",
|
||
project_id=None,
|
||
query_semantics={
|
||
"query_type": "existence",
|
||
"subject_focus": "project",
|
||
"scope_hint": "global",
|
||
"effective_source_policy": "internal",
|
||
},
|
||
)
|
||
|
||
assert plan is not None
|
||
assert plan["query_type"] == "existence"
|
||
assert plan["entity_type"] == "project"
|
||
assert plan["answer_contract"] == "yes_no_with_evidence"
|
||
assert plan["retrieval_policy"]["facts_first"] is True
|
||
assert plan["filters"] == []
|
||
|
||
|
||
def test_operation_contract_preserves_structured_existence_before_target_resolution() -> None:
|
||
class _FakeService(
|
||
AdaptiveRAGTargetResolutionMixin,
|
||
AdaptiveRAGAnswerMainlineMixin,
|
||
):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
contract = service._build_operation_contract(
|
||
question="帮我看看系统里面有没有开票系统。",
|
||
query_semantics={
|
||
"query_type": "existence",
|
||
"effective_source_policy": "internal",
|
||
"object_mentions": ["开票系统"],
|
||
},
|
||
)
|
||
|
||
assert contract["query_type"] == "existence"
|
||
assert contract["answer_contract"] == "yes_no_with_evidence"
|
||
assert contract["object_mentions"] == ["开票系统"]
|
||
|
||
|
||
def test_operation_contract_promotes_model_config_reference_to_status() -> None:
|
||
class _FakeService(
|
||
AdaptiveRAGTargetResolutionMixin,
|
||
AdaptiveRAGAnswerMainlineMixin,
|
||
):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
contract = service._build_operation_contract(
|
||
question="我们系统当前接的是哪个模型配置?",
|
||
query_semantics={
|
||
"query_type": "existence",
|
||
"effective_source_policy": "internal",
|
||
"object_mentions": ["模型配置"],
|
||
"query_profile": {
|
||
"referenced_objects": ["system_config", "model_routing"],
|
||
},
|
||
},
|
||
)
|
||
|
||
assert contract["query_type"] == "status"
|
||
assert contract["answer_contract"] == "entity_snapshot"
|
||
assert contract["object_mentions"] == ["模型配置"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_internal_target_resolution_prefers_project_candidate_for_existence_query() -> None:
|
||
class _FakeService(AdaptiveRAGTargetResolutionMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
async def _collect_internal_target_candidates(self, **kwargs):
|
||
_ = kwargs
|
||
return [
|
||
{
|
||
"target_entity_type": "project",
|
||
"target_ref": "project:6",
|
||
"resolved_label": "自动开票系统",
|
||
"score": 0.96,
|
||
"source_kind": "semantic_facts",
|
||
},
|
||
{
|
||
"target_entity_type": "feature",
|
||
"target_ref": "feature:/invoice",
|
||
"resolved_label": "开票系统",
|
||
"score": 0.82,
|
||
"source_kind": "feature_catalog",
|
||
},
|
||
]
|
||
|
||
service = _FakeService()
|
||
resolution = await service._resolve_internal_target(
|
||
question="帮我看看系统里面有没有开票系统。",
|
||
project_id=None,
|
||
query_semantics={"effective_source_policy": "internal"},
|
||
system_snapshot={},
|
||
operation_contract={
|
||
"query_type": "existence",
|
||
"answer_contract": "yes_no_with_evidence",
|
||
"object_mentions": ["开票系统"],
|
||
},
|
||
)
|
||
|
||
assert resolution["target_entity_type"] == "project"
|
||
assert resolution["target_ref"] == "project:6"
|
||
assert resolution["resolved_label"] == "自动开票系统"
|
||
assert resolution["resolution_mode"] == "deterministic"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_internal_target_resolution_prefers_session_followup_project_binding_before_semantic_filter() -> None:
|
||
class _FakeService(AdaptiveRAGTargetResolutionMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
async def _collect_internal_target_candidates(self, **kwargs):
|
||
_ = kwargs
|
||
return []
|
||
|
||
service = _FakeService()
|
||
resolution = await service._resolve_internal_target(
|
||
question="围绕“采购经理每日实时库存上报服务”回答:那个需求是否存在系统?",
|
||
project_id=None,
|
||
query_semantics={
|
||
"effective_source_policy": "internal",
|
||
"subject_focus": "project",
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["project_object"],
|
||
},
|
||
"session_followup_context": {
|
||
"rewrite_mode": "state_subject",
|
||
"last_subject": "采购经理每日实时库存上报服务",
|
||
"last_entity_ids": [123],
|
||
},
|
||
},
|
||
system_snapshot={},
|
||
operation_contract={
|
||
"query_type": "existence",
|
||
"answer_contract": "yes_no_with_evidence",
|
||
"object_mentions": ["采购经理每日实时库存上报服务"],
|
||
},
|
||
)
|
||
|
||
assert resolution["target_entity_type"] == "project"
|
||
assert resolution["target_ref"] == "project:123"
|
||
assert resolution["resolved_label"] == "采购经理每日实时库存上报服务"
|
||
assert resolution["resolution_mode"] == "deterministic"
|
||
assert resolution["binding_source"] == "session_followup_context"
|
||
|
||
|
||
def test_config_target_candidates_require_structured_referenced_objects() -> None:
|
||
class _FakeService(AdaptiveRAGTargetResolutionMixin):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
rows = service._collect_config_target_candidates(
|
||
query_semantics={
|
||
"query_profile": {
|
||
"referenced_objects": ["system_config", "model_routing"],
|
||
}
|
||
},
|
||
operation_contract={
|
||
"object_mentions": ["当前模型配置"],
|
||
},
|
||
)
|
||
|
||
assert len(rows) == 1
|
||
assert rows[0]["target_ref"] == "config:model_routing"
|
||
assert rows[0]["resolved_label"] == "当前模型配置"
|
||
|
||
|
||
def test_internal_query_runtime_metrics_include_contract_and_resolution_summaries() -> None:
|
||
class _FakeService(AdaptiveRAGTargetResolutionMixin):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
metrics = service._inject_internal_query_runtime_summaries(
|
||
retrieval_metrics={"db_query_ms": 12},
|
||
query_semantics={
|
||
"operation_contract": {
|
||
"query_type": "existence",
|
||
"answer_contract": "yes_no_with_evidence",
|
||
"object_mentions": ["开票系统"],
|
||
},
|
||
"target_resolution": {
|
||
"target_entity_type": "project",
|
||
"target_ref": "project:6",
|
||
"resolved_label": "自动开票系统",
|
||
"resolution_mode": "deterministic",
|
||
"confidence": 0.96,
|
||
},
|
||
"query_plan": {
|
||
"plan_version": "v2",
|
||
"query_type": "existence",
|
||
"entity_type": "project",
|
||
"target_ref": "project:6",
|
||
"answer_contract": "yes_no_with_evidence",
|
||
"filters": [{"field": "project_id", "operator": "equals", "value": 6}],
|
||
},
|
||
},
|
||
)
|
||
|
||
assert metrics["db_query_ms"] == 12
|
||
assert metrics["operation_contract_summary"]["query_type"] == "existence"
|
||
assert metrics["target_resolution_summary"]["target_ref"] == "project:6"
|
||
assert metrics["query_plan_summary"]["plan_version"] == "v2"
|
||
|
||
|
||
def test_internal_query_plan_uses_resolved_project_target_ref() -> None:
|
||
class _FakePlannerService(
|
||
AdaptiveRAGAnswerMainlineMixin,
|
||
AdaptiveRAGFastAnswerProjectUtilsMixin,
|
||
):
|
||
pass
|
||
|
||
service = _FakePlannerService()
|
||
plan = service._build_internal_query_plan(
|
||
question="帮我看看系统里面有没有开票系统。",
|
||
project_id=None,
|
||
query_semantics={
|
||
"query_type": "explain",
|
||
"subject_focus": "feature",
|
||
"scope_hint": "global",
|
||
"effective_source_policy": "internal",
|
||
},
|
||
operation_contract={
|
||
"query_type": "existence",
|
||
"answer_contract": "yes_no_with_evidence",
|
||
"object_mentions": ["开票系统"],
|
||
},
|
||
target_resolution={
|
||
"target_entity_type": "project",
|
||
"target_ref": "project:6",
|
||
"resolved_label": "自动开票系统",
|
||
"resolution_mode": "deterministic",
|
||
},
|
||
)
|
||
|
||
assert plan is not None
|
||
assert plan["plan_version"] == "v2"
|
||
assert plan["query_type"] == "existence"
|
||
assert plan["entity_type"] == "project"
|
||
assert plan["target_ref"] == "project:6"
|
||
assert plan["filters"][0]["field"] == "project_id"
|
||
assert plan["filters"][0]["value"] == 6
|
||
|
||
|
||
def test_internal_query_plan_skips_lexical_project_filter_fallback() -> None:
|
||
class _FakePlannerService(AdaptiveRAGAnswerMainlineMixin):
|
||
@staticmethod
|
||
def _extract_project_lookup_keyword(question: str) -> str: # pragma: no cover - should not be called
|
||
raise AssertionError(f"lexical keyword fallback should be skipped: {question}")
|
||
|
||
@staticmethod
|
||
def _extract_query_terms(question: str, *, preferred: str = "") -> list[str]: # pragma: no cover - should not be called
|
||
raise AssertionError(f"lexical term fallback should be skipped: {question}, {preferred}")
|
||
|
||
service = _FakePlannerService()
|
||
plan = service._build_internal_query_plan(
|
||
question="系统里有没有A股项目?",
|
||
project_id=None,
|
||
query_semantics={
|
||
"query_type": "existence",
|
||
"subject_focus": "project",
|
||
"scope_hint": "global",
|
||
"effective_source_policy": "internal",
|
||
},
|
||
operation_contract={
|
||
"query_type": "existence",
|
||
"answer_contract": "yes_no_with_evidence",
|
||
"object_mentions": [],
|
||
},
|
||
)
|
||
|
||
assert plan is not None
|
||
assert plan["filters"] == []
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_deterministic_query_prefers_project_created_at_answer() -> None:
|
||
calls: list[str] = []
|
||
|
||
class _FakeDeterministicService(AdaptiveRAGDeterministicQueryCoreMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
async def _try_fast_project_created_at_answer(
|
||
self,
|
||
*,
|
||
clean_q: str,
|
||
project_id,
|
||
query_semantics=None,
|
||
internal_context_packet=None,
|
||
):
|
||
calls.append("created_at")
|
||
_ = (clean_q, project_id, query_semantics, internal_context_packet)
|
||
return {"answer": "创建于 2026-03-01 09:30。", "sources": [], "missing_fields": []}
|
||
|
||
async def _try_fast_project_progress_answer(
|
||
self,
|
||
*,
|
||
clean_q: str,
|
||
project_id,
|
||
query_semantics=None,
|
||
internal_context_packet=None,
|
||
):
|
||
calls.append("progress")
|
||
_ = (clean_q, project_id, query_semantics, internal_context_packet)
|
||
return {"answer": "进度回答", "sources": []}
|
||
|
||
async def _try_fast_project_existence_answer(
|
||
self,
|
||
*,
|
||
clean_q: str,
|
||
project_id,
|
||
query_semantics=None,
|
||
internal_context_packet=None,
|
||
):
|
||
calls.append("existence")
|
||
_ = (clean_q, project_id, query_semantics, internal_context_packet)
|
||
return {"answer": "存在性回答", "sources": []}
|
||
|
||
service = _FakeDeterministicService()
|
||
result = await service._run_deterministic_query(
|
||
question="他是什么时候创建的?",
|
||
project_id=7,
|
||
query_type="status",
|
||
internal_context_packet=_make_internal_context_packet(
|
||
query_type="status",
|
||
answer_contract="entity_snapshot",
|
||
target_ref="project:7",
|
||
),
|
||
)
|
||
|
||
assert result is not None
|
||
assert "创建于 2026-03-01 09:30" in str(result["answer"])
|
||
assert calls[0] == "created_at"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_deterministic_query_runs_project_existence_for_existence_type() -> None:
|
||
calls: list[str] = []
|
||
|
||
class _FakeDeterministicService(AdaptiveRAGDeterministicQueryCoreMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
async def _try_fast_project_existence_answer(
|
||
self,
|
||
*,
|
||
clean_q: str,
|
||
project_id,
|
||
query_semantics=None,
|
||
internal_context_packet=None,
|
||
):
|
||
calls.append("existence")
|
||
_ = (clean_q, project_id, query_semantics, internal_context_packet)
|
||
return {"answer": "有,匹配到 1 个项目。", "sources": []}
|
||
|
||
service = _FakeDeterministicService()
|
||
result = await service._run_deterministic_query(
|
||
question="系统里有没有A股项目?",
|
||
project_id=None,
|
||
query_type="existence",
|
||
query_semantics={"subject_focus": "project"},
|
||
internal_context_packet=_make_internal_context_packet(
|
||
query_type="existence",
|
||
answer_contract="yes_no_with_evidence",
|
||
),
|
||
)
|
||
|
||
assert result is not None
|
||
assert "匹配到 1 个项目" in str(result["answer"])
|
||
assert calls == ["existence"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_deterministic_query_defers_internal_project_existence_to_semantic_snapshot() -> None:
|
||
calls: list[str] = []
|
||
|
||
class _FakeDeterministicService(AdaptiveRAGDeterministicQueryCoreMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
async def _try_fast_project_existence_answer(self, *, clean_q: str, project_id, query_semantics=None):
|
||
calls.append("existence")
|
||
_ = (clean_q, project_id, query_semantics)
|
||
return {"answer": "有,匹配到 1 个项目。", "sources": []}
|
||
|
||
service = _FakeDeterministicService()
|
||
result = await service._run_deterministic_query(
|
||
question="系统里有没有采购经理每日实时库存上报服务?",
|
||
project_id=None,
|
||
query_type="existence",
|
||
query_semantics={
|
||
"subject_focus": "project",
|
||
"source_policy": "internal",
|
||
"effective_source_policy": "internal",
|
||
"operation_contract": {
|
||
"query_type": "existence",
|
||
"answer_contract": "yes_no_with_evidence",
|
||
"object_mentions": ["采购经理每日实时库存上报服务"],
|
||
},
|
||
},
|
||
)
|
||
|
||
assert result is None
|
||
assert calls == []
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_project_existence_answer_prefers_resolved_project_target() -> None:
|
||
class _ScalarResult:
|
||
def __init__(self, row):
|
||
self._row = row
|
||
|
||
def scalar_one_or_none(self):
|
||
return self._row
|
||
|
||
class _FakeSession:
|
||
def __init__(self, row):
|
||
self._row = row
|
||
|
||
async def execute(self, stmt):
|
||
_ = stmt
|
||
return _ScalarResult(self._row)
|
||
|
||
project_row = SimpleNamespace(
|
||
id=72,
|
||
title="企业知识库问答AI项目",
|
||
name="企业知识库问答AI项目",
|
||
status="active",
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGFastAnswerProjectMainMixin, AdaptiveRAGFastAnswerProjectUtilsMixin):
|
||
def __init__(self) -> None:
|
||
self.db = _FakeSession(project_row)
|
||
self.tenant_id = 1
|
||
|
||
service = _FakeService()
|
||
result = await service._try_fast_project_existence_answer(
|
||
clean_q="系统里有没有知识库?",
|
||
project_id=None,
|
||
query_semantics={
|
||
"query_type": "existence",
|
||
"subject_focus": "unknown",
|
||
"target_resolution": {
|
||
"target_entity_type": "project",
|
||
"target_ref": "project:72",
|
||
"resolved_label": "企业知识库问答AI项目",
|
||
},
|
||
},
|
||
)
|
||
|
||
assert result is not None
|
||
assert "#72 企业知识库问答AI项目" in str(result["answer"])
|
||
assert result["sources"][0]["metadata"]["project_id"] == 72
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_project_existence_answer_uses_structured_mentions_without_lexical_extractors() -> None:
|
||
class _FakeService(
|
||
AdaptiveRAGFastAnswerProjectMainMixin,
|
||
AdaptiveRAGFastAnswerProjectUtilsMixin,
|
||
):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
@staticmethod
|
||
def _extract_project_lookup_keyword(question: str) -> str: # pragma: no cover - should not be called
|
||
raise AssertionError(f"lexical keyword fallback should be skipped: {question}")
|
||
|
||
@staticmethod
|
||
def _extract_query_terms(question: str, *, preferred: str = "") -> list[str]: # pragma: no cover - should not be called
|
||
raise AssertionError(f"lexical term fallback should be skipped: {question}, {preferred}")
|
||
|
||
async def _try_fast_project_existence_answer_from_semantic_facts(self, *, query_terms: list[str], project_id):
|
||
assert query_terms == ["A股项目"]
|
||
assert project_id is None
|
||
return {
|
||
"answer": "有,当前匹配到 1 个相关项目:\n#101 A股趋势打分工具(状态:active)",
|
||
"sources": [{"metadata": {"table": "semantic_facts", "match_count": 1}}],
|
||
}
|
||
|
||
async def _search_projects_by_keyword(self, *, keyword: str, project_id, limit: int): # pragma: no cover - should not be called
|
||
raise AssertionError(f"text fallback should be skipped: {keyword}, {project_id}, {limit}")
|
||
|
||
service = _FakeService()
|
||
result = await service._try_fast_project_existence_answer(
|
||
clean_q="系统里有没有A股项目?",
|
||
project_id=None,
|
||
query_semantics={
|
||
"query_type": "existence",
|
||
"subject_focus": "project",
|
||
"scope_hint": "global",
|
||
"operation_contract": {
|
||
"query_type": "existence",
|
||
"object_mentions": ["A股项目"],
|
||
},
|
||
},
|
||
)
|
||
|
||
assert result is not None
|
||
assert "#101 A股趋势打分工具" in str(result["answer"])
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_execute_mainline_skips_snapshot_direct_for_project_existence_target() -> None:
|
||
class _FakeContextCache:
|
||
async def get_system_snapshot(self, **kwargs):
|
||
_ = kwargs
|
||
return {"platform_profile": {"feature_catalog": [{"name": "智能配置", "route": "/settings/ai"}]}}
|
||
|
||
async def get_user_snapshot(self, **kwargs):
|
||
_ = kwargs
|
||
return {"projects": []}
|
||
|
||
class _FakeService(
|
||
AdaptiveRAGAnswerMainlineMixin,
|
||
AdaptiveRAGTargetResolutionMixin,
|
||
):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.context_cache = _FakeContextCache()
|
||
self.retrieval_called = False
|
||
|
||
def _plan_query_runtime(self, **kwargs):
|
||
_ = kwargs
|
||
return SimpleNamespace(
|
||
thinking_mode="normal",
|
||
retrieval_answer_mode="retrieval_normal",
|
||
budget=SimpleNamespace(max_steps=2, vector_top_k=3),
|
||
allow_fast_path=True,
|
||
reason="deterministic_query_type",
|
||
)
|
||
|
||
def _build_followup_clarification_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _build_route_clarification_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _build_meta_followup_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _maybe_return_clarification_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _maybe_return_meta_followup_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _get_active_external_qa_context(self):
|
||
return {}
|
||
|
||
def _build_operation_contract(self, *, question: str, query_semantics):
|
||
_ = (question, query_semantics)
|
||
return {
|
||
"query_type": "existence",
|
||
"answer_contract": "yes_no_with_evidence",
|
||
"object_mentions": ["开票系统"],
|
||
}
|
||
|
||
async def _resolve_internal_target(self, **kwargs):
|
||
_ = kwargs
|
||
return {
|
||
"target_entity_type": "project",
|
||
"target_ref": "project:6",
|
||
"resolved_label": "自动开票系统",
|
||
"resolution_mode": "deterministic",
|
||
"confidence": 0.96,
|
||
"candidate_summary": [],
|
||
}
|
||
|
||
async def _run_deterministic_query(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _build_retrieval_response(self, **kwargs):
|
||
self.retrieval_called = True
|
||
assert kwargs["query_type"] == "existence"
|
||
assert kwargs["query_semantics"]["target_resolution"]["target_ref"] == "project:6"
|
||
return {"processing_stage": "retrieval"}
|
||
|
||
def _build_snapshot_direct_answer(self, *args, **kwargs):
|
||
raise AssertionError("snapshot direct answer should be skipped for project existence target")
|
||
|
||
async def _maybe_return_snapshot_direct_response(self, **kwargs):
|
||
raise AssertionError("snapshot direct response should be skipped for project existence target")
|
||
|
||
async def _build_internal_model_config_fast_answer(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _try_glm_project_semantic_answer(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
service = _FakeService()
|
||
result = await service._execute_base_answer_mainline(
|
||
clean_q="帮我看看系统里面有没有开票系统。",
|
||
asked_by=1,
|
||
asked_via="pmflow",
|
||
project_id=None,
|
||
source_chat_id=None,
|
||
session_id=11,
|
||
turn_index=3,
|
||
context_session_id=11,
|
||
conversation=[],
|
||
state_row=None,
|
||
effective_q="帮我看看系统里面有没有开票系统。",
|
||
rewrite_meta={},
|
||
query_type="explain",
|
||
query_semantics={
|
||
"source_policy": "internal",
|
||
"effective_source_policy": "internal",
|
||
"subject_focus": "feature",
|
||
"object_mentions": ["开票系统"],
|
||
},
|
||
retrieval_domains=["db", "pmflow"],
|
||
scene_semantics={"scene_key": "ai_pmo", "snapshot_focus": "feature", "deep_intent": "none"},
|
||
thinking_mode="normal",
|
||
active_thinking_mode="normal",
|
||
deep_thinking=False,
|
||
enforce_scene_prompt_only=False,
|
||
deep_retrieval_steps=2,
|
||
deep_vector_top_k=3,
|
||
started=0.0,
|
||
message_intent="question",
|
||
mixed_signal_part=None,
|
||
consultant_mode_preference=None,
|
||
route_decision={
|
||
"route_mode": "internal",
|
||
"effective_source_policy": "internal",
|
||
"reply_contract": {"head": "内部知识库查询:", "tail_hint": None},
|
||
},
|
||
)
|
||
|
||
assert result["processing_stage"] == "retrieval"
|
||
assert service.retrieval_called is True
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_semantics_prefers_structured_model_output_over_hint_tables(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs):
|
||
_ = (prompt, kwargs)
|
||
return (
|
||
'{"intent":"question","question_part":"我感觉很多重复的项目在系统,你能列一下吗?",'
|
||
'"statement_part":"","query_type":"list","retrieval_domains":["db","graph"],'
|
||
'"subject_focus":"project","scope_hint":"global","feature_focus":"none"}'
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGCoreIntentMixin, AdaptiveRAGDeterministicQueryCoreMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
async def _fake_aplan_message(cls, *, message_text: str, source_system: str, metadata=None):
|
||
_ = (cls, message_text, source_system, metadata)
|
||
return WorkflowSemanticPlanner.deserialize_bundle(
|
||
{
|
||
"bundle_id": "semantic-query-type",
|
||
"semantic_version": "v1",
|
||
"source_system": "adaptive_rag",
|
||
"primary_workflow": "query_flow",
|
||
"workflows": [
|
||
{
|
||
"workflow_type": "query_flow",
|
||
"workflow_action": "answer",
|
||
"target_scope": "global",
|
||
"operation_codes": ["query.answer"],
|
||
"requires_confirmation": False,
|
||
"confidence": 0.93,
|
||
"clarification_needed": False,
|
||
"metadata": {},
|
||
}
|
||
],
|
||
"message_gate": "auto",
|
||
"reply_contract": {"mode": "text", "template_key": "workflow_bundle", "summary": "query_flow"},
|
||
"diagnostics": {"message_intent": "question"},
|
||
}
|
||
)
|
||
|
||
monkeypatch.setattr(WorkflowSemanticPlanner, "aplan_message", classmethod(_fake_aplan_message))
|
||
|
||
service = _FakeService()
|
||
semantics = await service._resolve_query_semantics(
|
||
message="我感觉很多重复的项目在系统,你能列一下吗?",
|
||
source_system="pmflow",
|
||
)
|
||
|
||
assert semantics["query_type"] == "list"
|
||
assert semantics["retrieval_domains"] == ["db", "graph"]
|
||
assert semantics["intent"] == "question"
|
||
assert semantics["subject_focus"] == "project"
|
||
assert semantics["scope_hint"] == "global"
|
||
assert semantics["feature_focus"] == "none"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_semantics_accepts_existence_query_type_from_structured_output(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs):
|
||
_ = (prompt, kwargs)
|
||
return (
|
||
'{"intent":"question","question_part":"系统里有没有A股项目?",'
|
||
'"statement_part":"","query_type":"existence","retrieval_domains":["db","graph"],'
|
||
'"object_mentions":["A股项目"],'
|
||
'"subject_focus":"project","scope_hint":"global","feature_focus":"none",'
|
||
'"query_profile":{"needs_private_context":true,"needs_freshness":false,"referenced_objects":["project_object"],"confidence":0.81},'
|
||
'"confidence":0.91}'
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGCoreIntentMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
async def _fake_aplan_message(cls, *, message_text: str, source_system: str, metadata=None):
|
||
_ = (cls, message_text, source_system, metadata)
|
||
return WorkflowSemanticPlanner.deserialize_bundle(
|
||
{
|
||
"bundle_id": "semantic-query-existence",
|
||
"semantic_version": "v1",
|
||
"source_system": "adaptive_rag",
|
||
"primary_workflow": "query_flow",
|
||
"workflows": [
|
||
{
|
||
"workflow_type": "query_flow",
|
||
"workflow_action": "answer",
|
||
"target_scope": "global",
|
||
"operation_codes": ["query.answer"],
|
||
"requires_confirmation": False,
|
||
"confidence": 0.89,
|
||
"clarification_needed": False,
|
||
"metadata": {},
|
||
}
|
||
],
|
||
"message_gate": "auto",
|
||
"reply_contract": {"mode": "text", "template_key": "workflow_bundle", "summary": "query_flow"},
|
||
"diagnostics": {"message_intent": "question"},
|
||
}
|
||
)
|
||
|
||
monkeypatch.setattr(WorkflowSemanticPlanner, "aplan_message", classmethod(_fake_aplan_message))
|
||
|
||
service = _FakeService()
|
||
semantics = await service._resolve_query_semantics(
|
||
message="系统里有没有A股项目?",
|
||
source_system="pmflow",
|
||
)
|
||
|
||
assert semantics["query_type"] == "existence"
|
||
assert semantics["object_mentions"] == ["A股项目"]
|
||
assert semantics["retrieval_domains"] == ["db", "graph"]
|
||
assert semantics["query_profile"]["referenced_objects"] == ["project_object"]
|
||
assert semantics["query_profile"]["needs_private_context"] is True
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_semantics_reuses_cached_result_for_identical_request(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
planner_calls: list[dict[str, object]] = []
|
||
llm_calls: list[str] = []
|
||
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs):
|
||
_ = kwargs
|
||
llm_calls.append(prompt)
|
||
return (
|
||
'{"intent":"question","question_part":"系统里有没有采购经理每日实时库存上报服务?",'
|
||
'"statement_part":"","query_type":"existence","retrieval_domains":["db","pmflow"],'
|
||
'"object_mentions":["采购经理每日实时库存上报服务"],'
|
||
'"subject_focus":"project","scope_hint":"global","feature_focus":"none",'
|
||
'"source_policy":"internal","effective_source_policy":"internal",'
|
||
'"query_profile":{"needs_private_context":true,"needs_freshness":false,"referenced_objects":["project_object"],"confidence":0.93},'
|
||
'"confidence":0.96}'
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGCoreIntentMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
async def _fake_aplan_message(cls, *, message_text: str, source_system: str, metadata=None):
|
||
_ = cls
|
||
planner_calls.append(
|
||
{
|
||
"message_text": message_text,
|
||
"source_system": source_system,
|
||
"metadata": dict(metadata or {}),
|
||
}
|
||
)
|
||
return WorkflowSemanticPlanner.deserialize_bundle(
|
||
{
|
||
"bundle_id": "semantic-query-cache",
|
||
"semantic_version": "v1",
|
||
"source_system": "adaptive_rag",
|
||
"primary_workflow": "query_flow",
|
||
"workflows": [
|
||
{
|
||
"workflow_type": "query_flow",
|
||
"workflow_action": "answer",
|
||
"target_scope": "global",
|
||
"operation_codes": ["query.answer"],
|
||
"requires_confirmation": False,
|
||
"confidence": 0.94,
|
||
"clarification_needed": False,
|
||
"metadata": {},
|
||
}
|
||
],
|
||
"message_gate": "auto",
|
||
"reply_contract": {"mode": "text", "template_key": "workflow_bundle", "summary": "query_flow"},
|
||
"diagnostics": {"message_intent": "question"},
|
||
}
|
||
)
|
||
|
||
monkeypatch.setattr(WorkflowSemanticPlanner, "aplan_message", classmethod(_fake_aplan_message))
|
||
|
||
service = _FakeService()
|
||
first = await service._resolve_query_semantics(
|
||
message="系统里有没有采购经理每日实时库存上报服务?",
|
||
source_system="pmflow",
|
||
metadata={"project_id": 1},
|
||
)
|
||
second = await service._resolve_query_semantics(
|
||
message="系统里有没有采购经理每日实时库存上报服务?",
|
||
source_system="pmflow",
|
||
metadata={"project_id": 1},
|
||
)
|
||
|
||
assert first == second
|
||
assert len(planner_calls) == 1
|
||
assert len(llm_calls) == 1
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_semantics_prompt_contract_guides_internal_project_source_policy(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs):
|
||
_ = kwargs
|
||
if '"source_policy"' in prompt and '"effective_source_policy"' in prompt:
|
||
return (
|
||
'{"intent":"question","question_part":"系统里有没有采购经理每日实时库存上报服务?",'
|
||
'"statement_part":"","query_type":"existence","retrieval_domains":["db","pmflow"],'
|
||
'"object_mentions":["采购经理每日实时库存上报服务"],'
|
||
'"subject_focus":"project","scope_hint":"global","feature_focus":"none",'
|
||
'"source_policy":"internal","effective_source_policy":"internal",'
|
||
'"query_profile":{"needs_private_context":true,"needs_freshness":false,"referenced_objects":["project_object"],"confidence":0.93},'
|
||
'"confidence":0.96}'
|
||
)
|
||
return (
|
||
'{"intent":"question","question_part":"系统里有没有采购经理每日实时库存上报服务?",'
|
||
'"statement_part":"","query_type":"existence","retrieval_domains":["db","pmflow"],'
|
||
'"object_mentions":["采购经理每日实时库存上报服务"],'
|
||
'"subject_focus":"project","scope_hint":"global","feature_focus":"none",'
|
||
'"query_profile":{"needs_private_context":true,"needs_freshness":false,"referenced_objects":["project_object"],"confidence":0.93},'
|
||
'"confidence":0.96}'
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGCoreIntentMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
async def _fake_aplan_message(cls, *, message_text: str, source_system: str, metadata=None):
|
||
_ = (cls, message_text, source_system, metadata)
|
||
return WorkflowSemanticPlanner.deserialize_bundle(
|
||
{
|
||
"bundle_id": "semantic-query-internal-project",
|
||
"semantic_version": "v1",
|
||
"source_system": "adaptive_rag",
|
||
"primary_workflow": "query_flow",
|
||
"workflows": [
|
||
{
|
||
"workflow_type": "query_flow",
|
||
"workflow_action": "answer",
|
||
"target_scope": "global",
|
||
"operation_codes": ["query.answer"],
|
||
"requires_confirmation": False,
|
||
"confidence": 0.94,
|
||
"clarification_needed": False,
|
||
"metadata": {},
|
||
}
|
||
],
|
||
"message_gate": "auto",
|
||
"reply_contract": {"mode": "text", "template_key": "workflow_bundle", "summary": "query_flow"},
|
||
"diagnostics": {"message_intent": "question"},
|
||
}
|
||
)
|
||
|
||
monkeypatch.setattr(WorkflowSemanticPlanner, "aplan_message", classmethod(_fake_aplan_message))
|
||
|
||
service = _FakeService()
|
||
semantics = await service._resolve_query_semantics(
|
||
message="系统里有没有采购经理每日实时库存上报服务?",
|
||
source_system="pmflow",
|
||
)
|
||
|
||
assert '"source_policy"' in PROMPT_RAG_QUERY_SEMANTICS
|
||
assert '"effective_source_policy"' in PROMPT_RAG_QUERY_SEMANTICS
|
||
assert semantics["source_policy"] == "internal"
|
||
assert semantics["effective_source_policy"] == "internal"
|
||
|
||
|
||
def test_feature_catalog_match_skips_lexical_query_term_fallback() -> None:
|
||
class _FakeService(AdaptiveRAGManifestFeatureMixinP1):
|
||
@staticmethod
|
||
def _extract_query_terms(question: str, *, preferred: str = "") -> list[str]: # pragma: no cover - should not be called
|
||
raise AssertionError(f"feature lexical term fallback should be skipped: {question}, {preferred}")
|
||
|
||
service = _FakeService()
|
||
rows = service._match_feature_catalog_entries(
|
||
question="智能配置入口在哪?",
|
||
system_snapshot={
|
||
"platform_profile": {
|
||
"feature_catalog": [
|
||
{
|
||
"name": "智能配置",
|
||
"route": "/ai-config",
|
||
"summary": "管理模型和提示词配置",
|
||
"aliases": ["模型设置"],
|
||
}
|
||
]
|
||
}
|
||
},
|
||
)
|
||
|
||
assert rows
|
||
assert rows[0]["name"] == "智能配置"
|
||
|
||
|
||
def test_feature_catalog_match_skips_generic_project_list_for_unrelated_project_mention() -> None:
|
||
class _FakeService(AdaptiveRAGManifestFeatureMixinP1):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
rows = service._match_feature_catalog_entries(
|
||
question="A股项目",
|
||
system_snapshot={
|
||
"platform_profile": {
|
||
"feature_catalog": [
|
||
{
|
||
"name": "项目列表",
|
||
"route": "/projects",
|
||
"summary": "查看全部项目",
|
||
},
|
||
{
|
||
"name": "智能配置",
|
||
"route": "/ai-config",
|
||
"summary": "管理模型配置",
|
||
},
|
||
]
|
||
}
|
||
},
|
||
)
|
||
|
||
assert rows == []
|
||
|
||
|
||
def test_internal_answer_contract_verifier_blocks_total_count_substitution_for_existence() -> None:
|
||
class _FakeVerifier(AdaptiveRAGAnswerPayloadMixin, AdaptiveRAGQueryRouteGatewayMixin):
|
||
pass
|
||
|
||
service = _FakeVerifier()
|
||
verdict = service._verify_internal_answer_contract(
|
||
query_type="existence",
|
||
answer="当前系统里项目总数为 42 个。",
|
||
sources=[
|
||
{
|
||
"source": "db",
|
||
"label": "Project database",
|
||
"score": 0.99,
|
||
"metadata": {"table": "projects"},
|
||
}
|
||
],
|
||
route_decision={"route_mode": "internal", "effective_source_policy": "internal"},
|
||
internal_context_packet=_make_internal_context_packet(
|
||
query_type="existence",
|
||
answer_contract="yes_no_with_evidence",
|
||
),
|
||
)
|
||
|
||
assert verdict["ok"] is False
|
||
assert verdict["reason_code"] == "no_total_count_substitution"
|
||
|
||
|
||
def test_internal_answer_contract_verifier_allows_project_snapshot_with_task_totals_for_existence() -> None:
|
||
class _FakeVerifier(AdaptiveRAGAnswerPayloadMixin, AdaptiveRAGQueryRouteGatewayMixin):
|
||
pass
|
||
|
||
service = _FakeVerifier()
|
||
verdict = service._verify_internal_answer_contract(
|
||
query_type="existence",
|
||
answer=(
|
||
"有。项目ID 123,项目名“采购经理每日实时库存上报服务”,状态 active,负责人 系统管理员,"
|
||
"最近更新 2026-04-13T14:27:59.301650,任务总数 4,已完成 0,进行中 0,待处理 4。"
|
||
),
|
||
sources=[
|
||
{
|
||
"source": "db",
|
||
"label": "Project database",
|
||
"score": 0.99,
|
||
"metadata": {"table": "projects", "matched_project_ids": [123]},
|
||
}
|
||
],
|
||
route_decision={"route_mode": "internal", "effective_source_policy": "internal"},
|
||
internal_context_packet=_make_internal_context_packet(
|
||
query_type="existence",
|
||
answer_contract="yes_no_with_evidence",
|
||
target_ref="project:123",
|
||
),
|
||
)
|
||
|
||
assert verdict["ok"] is True
|
||
assert verdict["reason_code"] is None
|
||
|
||
|
||
def test_internal_answer_contract_verifier_allows_evidence_backed_existence_answer() -> None:
|
||
class _FakeVerifier(AdaptiveRAGAnswerPayloadMixin, AdaptiveRAGQueryRouteGatewayMixin):
|
||
pass
|
||
|
||
service = _FakeVerifier()
|
||
verdict = service._verify_internal_answer_contract(
|
||
query_type="existence",
|
||
answer="有,当前匹配到 1 个相关项目:#101 A股趋势打分工具(状态:active)",
|
||
sources=[
|
||
{
|
||
"source": "db",
|
||
"label": "Project database",
|
||
"score": 0.99,
|
||
"metadata": {"table": "projects", "match_count": 1},
|
||
}
|
||
],
|
||
route_decision={"route_mode": "internal", "effective_source_policy": "internal"},
|
||
internal_context_packet=_make_internal_context_packet(
|
||
query_type="existence",
|
||
answer_contract="yes_no_with_evidence",
|
||
target_ref="project:101",
|
||
),
|
||
)
|
||
|
||
assert verdict["ok"] is True
|
||
assert verdict["reason_code"] is None
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_classify_intent_prefers_structured_semantics_without_rule_split(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs):
|
||
_ = (prompt, kwargs)
|
||
return (
|
||
'{"intent":"mixed","question_part":"现在进展如何?","statement_part":"并新增任务确认流",'
|
||
'"query_type":"status","retrieval_domains":["db","graph","pmflow"]}'
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGCoreIntentMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
async def _fake_aplan_message(cls, *, message_text: str, source_system: str, metadata=None):
|
||
_ = (cls, message_text, source_system, metadata)
|
||
return WorkflowSemanticPlanner.deserialize_bundle(
|
||
{
|
||
"bundle_id": "semantic-intent",
|
||
"semantic_version": "v1",
|
||
"source_system": "adaptive_rag",
|
||
"primary_workflow": "query_flow",
|
||
"workflows": [
|
||
{
|
||
"workflow_type": "query_flow",
|
||
"workflow_action": "answer",
|
||
"target_scope": "global",
|
||
"operation_codes": ["query.answer"],
|
||
"requires_confirmation": False,
|
||
"confidence": 0.88,
|
||
"clarification_needed": False,
|
||
"metadata": {},
|
||
}
|
||
],
|
||
"message_gate": "auto",
|
||
"reply_contract": {"mode": "text", "template_key": "workflow_bundle", "summary": "query_flow"},
|
||
"diagnostics": {"message_intent": "business_review"},
|
||
}
|
||
)
|
||
|
||
monkeypatch.setattr(WorkflowSemanticPlanner, "aplan_message", classmethod(_fake_aplan_message))
|
||
|
||
service = _FakeService()
|
||
result = await service.classify_intent("现在进展如何?并新增任务确认流")
|
||
|
||
assert result == {
|
||
"intent": "mixed",
|
||
"confidence": 0.88,
|
||
"question_part": "现在进展如何?",
|
||
"statement_part": "并新增任务确认流",
|
||
}
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_vector_retrieval_skips_legacy_fallback_for_non_postgresql_session() -> None:
|
||
class _FakeLLM:
|
||
async def get_embedding_async(self, question: str, **kwargs):
|
||
_ = (question, kwargs)
|
||
return [0.1, 0.2, 0.3]
|
||
|
||
class _FakeService(AdaptiveRAGRetrievalVectorMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
self.legacy_called = False
|
||
|
||
def _map_domains_to_source_types(self, domains: list[str]) -> list[str]:
|
||
return list(domains)
|
||
|
||
def _is_postgresql_session(self) -> bool:
|
||
return False
|
||
|
||
async def _retrieve_vector_context_legacy(self, **kwargs):
|
||
_ = kwargs
|
||
self.legacy_called = True
|
||
return ["legacy-hit"]
|
||
|
||
service = _FakeService()
|
||
result = await service._retrieve_vector_context(
|
||
"查询项目文档",
|
||
domains=["doc"],
|
||
project_id=None,
|
||
source_chat_id=None,
|
||
)
|
||
|
||
assert result == []
|
||
assert service.legacy_called is False
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_vector_retrieval_skips_legacy_fallback_when_pg_query_errors() -> None:
|
||
class _FakeLLM:
|
||
async def get_embedding_async(self, question: str, **kwargs):
|
||
_ = (question, kwargs)
|
||
return [0.1, 0.2, 0.3]
|
||
|
||
class _FakeService(AdaptiveRAGRetrievalVectorMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
self.legacy_called = False
|
||
|
||
def _map_domains_to_source_types(self, domains: list[str]) -> list[str]:
|
||
return list(domains)
|
||
|
||
def _is_postgresql_session(self) -> bool:
|
||
return True
|
||
|
||
async def _retrieve_vector_context_pg(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _retrieve_vector_context_legacy(self, **kwargs):
|
||
_ = kwargs
|
||
self.legacy_called = True
|
||
return ["legacy-hit"]
|
||
|
||
service = _FakeService()
|
||
result = await service._retrieve_vector_context(
|
||
"查询项目文档",
|
||
domains=["doc"],
|
||
project_id=None,
|
||
source_chat_id=None,
|
||
)
|
||
|
||
assert result == []
|
||
assert service.legacy_called is False
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_deep_guard_answer_raises_on_empty_llm_response() -> None:
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs):
|
||
_ = (prompt, kwargs)
|
||
return ""
|
||
|
||
class _FakeService(AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
def _normalize_thinking_mode(self, thinking_mode: str) -> str:
|
||
return "normal" if str(thinking_mode or "").strip().lower() != "deep" else "deep"
|
||
|
||
async def get_project_manifest(self, project_id):
|
||
_ = project_id
|
||
return {}
|
||
|
||
async def _select_scene_prompt(self, **kwargs):
|
||
_ = kwargs
|
||
return "external_trend", "请回答问题"
|
||
|
||
def _enforce_feature_evidence_in_answer(self, **kwargs):
|
||
return str(kwargs.get("answer_text") or "")
|
||
|
||
service = _FakeService()
|
||
|
||
with pytest.raises(ValueError, match="adaptive_rag_answer_empty_response"):
|
||
await service._generate_answer(
|
||
question="最近AI行业有什么动态",
|
||
retrieved=[],
|
||
conversation=[],
|
||
project_id=None,
|
||
asked_via="feishu",
|
||
)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_deep_guard_answer_uses_low_first_routing_meta_for_deep_thinking() -> None:
|
||
class _FakeLLM:
|
||
def __init__(self) -> None:
|
||
self.kwargs: dict[str, object] = {}
|
||
|
||
async def generate(self, prompt: str, **kwargs):
|
||
_ = prompt
|
||
self.kwargs = dict(kwargs)
|
||
return "low-tier answer"
|
||
|
||
class _FakeService(AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
def _normalize_thinking_mode(self, thinking_mode: str) -> str:
|
||
return "deep" if str(thinking_mode or "").strip().lower() == "deep" else "normal"
|
||
|
||
async def get_project_manifest(self, project_id):
|
||
_ = project_id
|
||
return {}
|
||
|
||
async def _select_scene_prompt(self, **kwargs):
|
||
_ = kwargs
|
||
return "external_trend", "请回答问题"
|
||
|
||
def _enforce_feature_evidence_in_answer(self, **kwargs):
|
||
return str(kwargs.get("answer_text") or "")
|
||
|
||
service = _FakeService()
|
||
result = await service._generate_answer(
|
||
question="最近AI行业有什么动态",
|
||
retrieved=[],
|
||
conversation=[],
|
||
project_id=None,
|
||
asked_via="feishu",
|
||
thinking_mode="deep",
|
||
)
|
||
|
||
assert result == "low-tier answer"
|
||
assert service.llm.kwargs["function_name"] == "adaptive_rag_answer_deep"
|
||
routing_meta = service.llm.kwargs["routing_meta"]
|
||
assert isinstance(routing_meta, dict)
|
||
assert routing_meta["tier_alias"] == "low"
|
||
assert routing_meta["planned_tier"] == "low"
|
||
assert routing_meta["response_contract"] == "complete"
|
||
assert routing_meta["fallback_policy"] == "low_to_high"
|
||
assert routing_meta["execution_mode"] == "query"
|
||
assert routing_meta["routing_decision"] == "force_low_first"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_deep_guard_answer_external_route_uses_external_presenter_without_internal_context() -> None:
|
||
class _FakePresenter:
|
||
def __init__(self) -> None:
|
||
self.calls: list[dict[str, object]] = []
|
||
|
||
async def present(self, *, question: str, context_packet: dict, thinking_mode: str, answer_max_tokens=None):
|
||
self.calls.append(
|
||
{
|
||
"question": question,
|
||
"context_packet": dict(context_packet),
|
||
"thinking_mode": thinking_mode,
|
||
"answer_max_tokens": answer_max_tokens,
|
||
}
|
||
)
|
||
return {
|
||
"answer_text": "只基于外部证据生成的回答",
|
||
"prompt_variant_id": "external_summary_with_sources",
|
||
}
|
||
|
||
class _FakeService(AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = object()
|
||
self.presenter = _FakePresenter()
|
||
self.prompt_variant = "unknown"
|
||
|
||
def _normalize_thinking_mode(self, thinking_mode: str) -> str:
|
||
_ = thinking_mode
|
||
return "normal"
|
||
|
||
async def get_project_manifest(self, project_id): # pragma: no cover - should not be called
|
||
raise AssertionError(f"manifest should be skipped for external route: {project_id}")
|
||
|
||
async def _resolve_answer_scene_semantics(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"scene semantics should be skipped for external route: {kwargs}")
|
||
|
||
async def _select_scene_prompt(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"scene prompt should be skipped for external route: {kwargs}")
|
||
|
||
def _build_external_answer_presenter(self):
|
||
return self.presenter
|
||
|
||
def _remember_answer_prompt_variant(self, prompt_variant_id: str) -> None:
|
||
self.prompt_variant = prompt_variant_id
|
||
|
||
def _enforce_feature_evidence_in_answer(self, **kwargs):
|
||
return str(kwargs.get("answer_text") or "")
|
||
|
||
service = _FakeService()
|
||
|
||
result = await service._generate_answer(
|
||
question="最近 AI 编程模型有什么新动态?",
|
||
retrieved=[],
|
||
conversation=[],
|
||
project_id=7,
|
||
asked_via="feishu",
|
||
query_semantics={"effective_source_policy": "external"},
|
||
external_context_packet={
|
||
"question": "最近 AI 编程模型有什么新动态?",
|
||
"plan": {
|
||
"answer_contract": "summary_with_sources",
|
||
"time_range_hint": "最近",
|
||
},
|
||
"adapter_attempts": [{"adapter": "live_search", "status": "success"}],
|
||
"verified_evidence": [
|
||
{
|
||
"source_type": "url",
|
||
"source_label": "OpenAI Developers",
|
||
"title": "Models - Codex | OpenAI Developers",
|
||
"target_url": "https://developers.openai.com/codex/models",
|
||
"snippet": "Latest: GPT-5.4",
|
||
}
|
||
],
|
||
"verifier": {"ok": True},
|
||
"uncertainties": [],
|
||
},
|
||
)
|
||
|
||
assert result == "只基于外部证据生成的回答"
|
||
assert service.prompt_variant == "external_summary_with_sources"
|
||
assert len(service.presenter.calls) == 1
|
||
assert service.presenter.calls[0]["question"] == "最近 AI 编程模型有什么新动态?"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_retrieval_response_external_insufficient_evidence_keeps_external_clarify_contract() -> None:
|
||
class _FakeDB:
|
||
def add(self, obj) -> None:
|
||
obj.id = 1
|
||
|
||
async def flush(self) -> None:
|
||
return None
|
||
|
||
async def commit(self) -> None:
|
||
return None
|
||
|
||
async def refresh(self, obj) -> None:
|
||
_ = obj
|
||
return None
|
||
|
||
class _FakeContextCache:
|
||
async def append_session_messages(self, **kwargs) -> None:
|
||
_ = kwargs
|
||
return None
|
||
|
||
class _FakeService(
|
||
AdaptiveRAGAnswerRetrievalMixin,
|
||
AdaptiveRAGAnswerPayloadMixin,
|
||
AdaptiveRAGQueryRouteGatewayMixin,
|
||
AdaptiveRAGDeepGuardRulesMixin,
|
||
):
|
||
def __init__(self) -> None:
|
||
self.db = _FakeDB()
|
||
self.context_cache = _FakeContextCache()
|
||
self.tenant_id = 1
|
||
|
||
def _normalize_thinking_mode(self, thinking_mode: str) -> str:
|
||
return "deep" if str(thinking_mode or "").strip().lower() == "deep" else "normal"
|
||
|
||
async def _execute_external_query_gateway(self, **kwargs):
|
||
_ = kwargs
|
||
return [], {
|
||
"mode": "external_query_gateway_verifier_blocked",
|
||
"external_context_packet": {
|
||
"question": "OpenAI 最近发布了哪些新模型?",
|
||
"plan": {
|
||
"answer_contract": "summary_with_sources",
|
||
},
|
||
"adapter_attempts": [{"adapter": "live_search", "status": "empty"}],
|
||
"verified_evidence": [],
|
||
"verifier": {
|
||
"ok": False,
|
||
"reason_code": "insufficient_evidence",
|
||
"missing_fields": ["external_evidence"],
|
||
},
|
||
"uncertainties": ["insufficient_evidence"],
|
||
},
|
||
"tool_trace": [],
|
||
"domains_used": ["url"],
|
||
}
|
||
|
||
async def _generate_answer(self, *args, **kwargs):
|
||
_ = (args, kwargs)
|
||
return "当前关于“OpenAI 最近发布了哪些新模型?”的外部证据不足。请补充更明确的对象、范围或继续提供可核验来源后,我再继续分析。"
|
||
|
||
def _peek_answer_prompt_variant(self):
|
||
return "external_clarify"
|
||
|
||
def _peek_last_answer_routing_snapshot(self):
|
||
return {}
|
||
|
||
def _dedupe_sources(self, retrieved):
|
||
_ = retrieved
|
||
return []
|
||
|
||
def _build_qa_record_runtime_fields(self, **kwargs):
|
||
return {
|
||
"policy_version": "test",
|
||
"citations": [],
|
||
"sources_used": list(kwargs.get("sources") or []),
|
||
"evidence_pack": {},
|
||
"tool_trace": list(kwargs.get("tool_trace") or []),
|
||
}
|
||
|
||
def _build_qa_record_external_fields(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
async def _update_session_state_after_turn(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _build_response_payload(self, **kwargs):
|
||
return kwargs
|
||
|
||
service = _FakeService()
|
||
result = await service._build_retrieval_response(
|
||
clean_q="OpenAI 最近发布了哪些新模型?",
|
||
effective_q="OpenAI 最近发布了哪些新模型?",
|
||
asked_by=1,
|
||
asked_via="feishu",
|
||
project_id=None,
|
||
source_chat_id=None,
|
||
session_id=11,
|
||
turn_index=2,
|
||
context_session_id=11,
|
||
conversation=[],
|
||
state_row=None,
|
||
rewrite_meta={"mode": "direct"},
|
||
query_type="explain",
|
||
query_semantics={"effective_source_policy": "external"},
|
||
retrieval_domains=["url"],
|
||
scene_semantics={"scene_key": "default", "deep_intent": "none"},
|
||
thinking_mode="normal",
|
||
active_thinking_mode="normal",
|
||
deep_thinking=False,
|
||
forced_deep_reason="",
|
||
deep_retrieval_steps=2,
|
||
deep_vector_top_k=3,
|
||
system_snapshot={},
|
||
user_snapshot=None,
|
||
started=0.0,
|
||
message_intent=None,
|
||
mixed_signal_part=None,
|
||
answer_mode="retrieval_normal",
|
||
consultant_mode_preference=None,
|
||
route_decision={
|
||
"route_mode": "external",
|
||
"effective_source_policy": "external",
|
||
"decision_origin": "query_semantics_contract",
|
||
"reply_contract": {"head": "外部实时查询:", "tail_hint": None},
|
||
"query_profile": {"needs_freshness": True, "referenced_objects": ["public_api"]},
|
||
},
|
||
)
|
||
|
||
assert result["answer_mode"] == "clarify"
|
||
assert result["grounding_status"] == "insufficient_evidence"
|
||
assert result["sources"] == []
|
||
assert result["sources_used"] == []
|
||
assert "系统快照" not in result["answer"]
|
||
assert "外部证据不足" in result["answer"]
|
||
assert result["answer"].startswith("外部实时查询:")
|
||
assert result["missing_fields"] == ["external_evidence"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_retrieval_response_exposes_fit_brief_and_external_recommendation_blocks() -> None:
|
||
class _FakeDB:
|
||
def add(self, obj) -> None:
|
||
obj.id = 2
|
||
|
||
async def flush(self) -> None:
|
||
return None
|
||
|
||
async def commit(self) -> None:
|
||
return None
|
||
|
||
async def refresh(self, obj) -> None:
|
||
_ = obj
|
||
return None
|
||
|
||
class _FakeContextCache:
|
||
async def append_session_messages(self, **kwargs) -> None:
|
||
_ = kwargs
|
||
return None
|
||
|
||
class _FakeService(
|
||
AdaptiveRAGAnswerRetrievalMixin,
|
||
AdaptiveRAGAnswerPayloadMixin,
|
||
AdaptiveRAGQueryRouteGatewayMixin,
|
||
AdaptiveRAGDeepGuardRulesMixin,
|
||
):
|
||
def __init__(self) -> None:
|
||
self.db = _FakeDB()
|
||
self.context_cache = _FakeContextCache()
|
||
self.tenant_id = 1
|
||
self.fit_brief_seen = None
|
||
|
||
def _normalize_thinking_mode(self, thinking_mode: str) -> str:
|
||
return "deep" if str(thinking_mode or "").strip().lower() == "deep" else "normal"
|
||
|
||
def _to_float(self, value, default=0.0):
|
||
try:
|
||
return float(value)
|
||
except Exception:
|
||
return float(default)
|
||
|
||
def _build_policy_version(self, *, prompt_variant_id: str | None = None) -> str:
|
||
return f"retrieval-v2.1|routing:unknown|prompt:{prompt_variant_id or 'unknown'}"
|
||
|
||
def _normalize_query_route_decision(self, payload):
|
||
return dict(payload or {})
|
||
|
||
async def _build_project_fit_brief(self, **kwargs):
|
||
_ = kwargs
|
||
return {
|
||
"project_summary": "当前项目:PMFlow AI 管家,聚焦项目内问答与需求流转。",
|
||
"repo_summary": "当前仓库:FastAPI 后端 + React 前端。",
|
||
"current_capabilities": ["AI 管家", "智能配置"],
|
||
"constraints": ["只允许基于外部公开证据给出推荐。"],
|
||
"recommendation_focus": ["判断 Responses API 是否适合当前项目接入"],
|
||
}
|
||
|
||
async def _execute_external_query_gateway(self, **kwargs):
|
||
self.fit_brief_seen = kwargs.get("fit_brief")
|
||
return [
|
||
RetrievedItem(
|
||
source="url",
|
||
content="Responses API 支持工具调用与结构化输出。",
|
||
score=0.93,
|
||
source_label="OpenAI Docs",
|
||
metadata={
|
||
"target_url": "https://platform.openai.com/docs/guides/responses",
|
||
},
|
||
)
|
||
], {
|
||
"mode": "external_query_gateway",
|
||
"external_context_packet": {
|
||
"question": "这个项目适不适合接 OpenAI Responses API?",
|
||
"plan": {
|
||
"answer_contract": "summary_with_sources",
|
||
"topic": "Responses API 适配性",
|
||
},
|
||
"adapter_attempts": [{"adapter": "live_search", "status": "success"}],
|
||
"verified_evidence": [
|
||
{
|
||
"source_type": "url",
|
||
"source_label": "OpenAI Docs",
|
||
"title": "Responses API guide",
|
||
"target_url": "https://platform.openai.com/docs/guides/responses",
|
||
"snippet": "Responses API supports tool calls and structured outputs.",
|
||
}
|
||
],
|
||
"verifier": {"ok": True},
|
||
"uncertainties": [],
|
||
"retrieval_status": "evidence_found",
|
||
},
|
||
"tool_trace": [],
|
||
"domains_used": ["url"],
|
||
}
|
||
|
||
async def _generate_answer(self, *args, **kwargs):
|
||
_ = (args, kwargs)
|
||
return "外部公开资料显示 Responses API 值得关注,但目前还需要先做项目映射验证。"
|
||
|
||
def _peek_answer_prompt_variant(self):
|
||
return "external_summary_with_sources"
|
||
|
||
def _peek_last_answer_routing_snapshot(self):
|
||
return {}
|
||
|
||
def _dedupe_sources(self, retrieved):
|
||
_ = retrieved
|
||
return [
|
||
{
|
||
"source": "url",
|
||
"label": "OpenAI Docs",
|
||
"score": 0.93,
|
||
"metadata": {
|
||
"target_url": "https://platform.openai.com/docs/guides/responses",
|
||
},
|
||
}
|
||
]
|
||
|
||
def _build_qa_record_runtime_fields(self, **kwargs):
|
||
return {
|
||
"policy_version": "test",
|
||
"citations": [],
|
||
"sources_used": list(kwargs.get("sources") or []),
|
||
"evidence_pack": {},
|
||
"tool_trace": list(kwargs.get("tool_trace") or []),
|
||
}
|
||
|
||
def _build_qa_record_external_fields(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
async def _update_session_state_after_turn(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
service = _FakeService()
|
||
result = await service._build_retrieval_response(
|
||
clean_q="这个项目适不适合接 OpenAI Responses API?",
|
||
effective_q="这个项目适不适合接 OpenAI Responses API?",
|
||
asked_by=1,
|
||
asked_via="feishu",
|
||
project_id=17,
|
||
source_chat_id=None,
|
||
session_id=12,
|
||
turn_index=1,
|
||
context_session_id=12,
|
||
conversation=[],
|
||
state_row=None,
|
||
rewrite_meta={"mode": "direct"},
|
||
query_type="explain",
|
||
query_semantics={
|
||
"effective_source_policy": "external",
|
||
"project_operating_context": {
|
||
"current_project_id": 17,
|
||
"current_repo_scope": "pmflow-main",
|
||
"page_anchor": {"project_id": 17},
|
||
"thread_anchor": {},
|
||
"fit_scope": "current_project_and_repo",
|
||
"decision_strategy": "answer_then_offer_plan",
|
||
},
|
||
"execution_recipe": {
|
||
"primary_lane": "external",
|
||
"fit_brief_required": True,
|
||
"external_evidence_required": True,
|
||
"plan_handoff_mode": "offer_after_answer",
|
||
},
|
||
},
|
||
retrieval_domains=["url"],
|
||
scene_semantics={"scene_key": "default", "deep_intent": "none"},
|
||
thinking_mode="normal",
|
||
active_thinking_mode="normal",
|
||
deep_thinking=False,
|
||
forced_deep_reason="",
|
||
deep_retrieval_steps=2,
|
||
deep_vector_top_k=3,
|
||
system_snapshot={},
|
||
user_snapshot=None,
|
||
started=time.perf_counter(),
|
||
message_intent=None,
|
||
mixed_signal_part=None,
|
||
answer_mode="retrieval_normal",
|
||
consultant_mode_preference=None,
|
||
route_decision={
|
||
"route_mode": "external",
|
||
"effective_source_policy": "external",
|
||
"decision_origin": "query_semantics_contract",
|
||
"reply_contract": {"head": "外部实时查询:", "tail_hint": None},
|
||
"query_profile": {"needs_freshness": True, "referenced_objects": ["public_api"]},
|
||
},
|
||
)
|
||
|
||
assert service.fit_brief_seen is not None
|
||
assert result["fit_brief"]["project_summary"].startswith("当前项目:PMFlow AI 管家")
|
||
assert result["fit_brief"]["current_capabilities"] == ["AI 管家", "智能配置"]
|
||
assert result["external_recommendation"]["retrieval_status"] == "evidence_found"
|
||
assert result["external_recommendation"]["impact_status"] == "indirect_only"
|
||
assert result["external_recommendation"]["affected_signal_sources"] == []
|
||
assert "映射验证" in str(result["external_recommendation"]["external_action"])
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_retrieval_response_can_disable_fit_brief_external_recommendation(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
class _FakeDB:
|
||
def add(self, obj) -> None:
|
||
obj.id = 2
|
||
|
||
async def flush(self) -> None:
|
||
return None
|
||
|
||
async def commit(self) -> None:
|
||
return None
|
||
|
||
async def refresh(self, obj) -> None:
|
||
_ = obj
|
||
return None
|
||
|
||
class _FakeContextCache:
|
||
async def append_session_messages(self, **kwargs) -> None:
|
||
_ = kwargs
|
||
return None
|
||
|
||
class _FakeService(
|
||
AdaptiveRAGAnswerRetrievalMixin,
|
||
AdaptiveRAGAnswerPayloadMixin,
|
||
AdaptiveRAGQueryRouteGatewayMixin,
|
||
AdaptiveRAGDeepGuardRulesMixin,
|
||
):
|
||
def __init__(self) -> None:
|
||
self.db = _FakeDB()
|
||
self.context_cache = _FakeContextCache()
|
||
self.tenant_id = 1
|
||
self.fit_brief_seen = "unset"
|
||
|
||
def _normalize_thinking_mode(self, thinking_mode: str) -> str:
|
||
return "deep" if str(thinking_mode or "").strip().lower() == "deep" else "normal"
|
||
|
||
def _to_float(self, value, default=0.0):
|
||
try:
|
||
return float(value)
|
||
except Exception:
|
||
return float(default)
|
||
|
||
def _build_policy_version(self, *, prompt_variant_id: str | None = None) -> str:
|
||
return f"retrieval-v2.1|routing:unknown|prompt:{prompt_variant_id or 'unknown'}"
|
||
|
||
def _normalize_query_route_decision(self, payload):
|
||
return dict(payload or {})
|
||
|
||
async def _build_project_fit_brief(self, **kwargs):
|
||
_ = kwargs
|
||
raise AssertionError("fit brief builder should be bypassed when rollback flag is off")
|
||
|
||
async def _execute_external_query_gateway(self, **kwargs):
|
||
self.fit_brief_seen = kwargs.get("fit_brief")
|
||
return [
|
||
RetrievedItem(
|
||
source="url",
|
||
content="Responses API 支持工具调用与结构化输出。",
|
||
score=0.93,
|
||
source_label="OpenAI Docs",
|
||
metadata={
|
||
"target_url": "https://platform.openai.com/docs/guides/responses",
|
||
},
|
||
)
|
||
], {
|
||
"mode": "external_query_gateway",
|
||
"external_context_packet": {
|
||
"question": "这个项目适不适合接 OpenAI Responses API?",
|
||
"plan": {
|
||
"answer_contract": "summary_with_sources",
|
||
"topic": "Responses API 适配性",
|
||
},
|
||
"adapter_attempts": [{"adapter": "live_search", "status": "success"}],
|
||
"verified_evidence": [
|
||
{
|
||
"source_type": "url",
|
||
"source_label": "OpenAI Docs",
|
||
"title": "Responses API guide",
|
||
"target_url": "https://platform.openai.com/docs/guides/responses",
|
||
"snippet": "Responses API supports tool calls and structured outputs.",
|
||
}
|
||
],
|
||
"verifier": {"ok": True},
|
||
"uncertainties": [],
|
||
"retrieval_status": "evidence_found",
|
||
},
|
||
"tool_trace": [],
|
||
"domains_used": ["url"],
|
||
}
|
||
|
||
async def _generate_answer(self, *args, **kwargs):
|
||
_ = (args, kwargs)
|
||
return "外部公开资料显示 Responses API 值得关注,但目前还需要先做项目映射验证。"
|
||
|
||
def _peek_answer_prompt_variant(self):
|
||
return "external_summary_with_sources"
|
||
|
||
def _peek_last_answer_routing_snapshot(self):
|
||
return {}
|
||
|
||
def _dedupe_sources(self, retrieved):
|
||
_ = retrieved
|
||
return [
|
||
{
|
||
"source": "url",
|
||
"label": "OpenAI Docs",
|
||
"score": 0.93,
|
||
"metadata": {
|
||
"target_url": "https://platform.openai.com/docs/guides/responses",
|
||
},
|
||
}
|
||
]
|
||
|
||
def _build_qa_record_runtime_fields(self, **kwargs):
|
||
return {
|
||
"policy_version": "test",
|
||
"citations": [],
|
||
"sources_used": list(kwargs.get("sources") or []),
|
||
"evidence_pack": {},
|
||
"tool_trace": list(kwargs.get("tool_trace") or []),
|
||
}
|
||
|
||
def _build_qa_record_external_fields(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
async def _update_session_state_after_turn(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
monkeypatch.setenv("PMFLOW_ENABLE_FIT_BRIEF_EXTERNAL_RECOMMENDATION", "0")
|
||
service = _FakeService()
|
||
result = await service._build_retrieval_response(
|
||
clean_q="这个项目适不适合接 OpenAI Responses API?",
|
||
effective_q="这个项目适不适合接 OpenAI Responses API?",
|
||
asked_by=1,
|
||
asked_via="feishu",
|
||
project_id=17,
|
||
source_chat_id=None,
|
||
session_id=12,
|
||
turn_index=1,
|
||
context_session_id=12,
|
||
conversation=[],
|
||
state_row=None,
|
||
rewrite_meta={"mode": "direct"},
|
||
query_type="explain",
|
||
query_semantics={
|
||
"effective_source_policy": "external",
|
||
"project_operating_context": {
|
||
"current_project_id": 17,
|
||
"current_repo_scope": "pmflow-main",
|
||
"page_anchor": {"project_id": 17},
|
||
"thread_anchor": {},
|
||
"fit_scope": "current_project_and_repo",
|
||
"decision_strategy": "answer_then_offer_plan",
|
||
},
|
||
"execution_recipe": {
|
||
"primary_lane": "external",
|
||
"fit_brief_required": True,
|
||
"external_evidence_required": True,
|
||
"plan_handoff_mode": "offer_after_answer",
|
||
},
|
||
},
|
||
retrieval_domains=["url"],
|
||
scene_semantics={"scene_key": "default", "deep_intent": "none"},
|
||
thinking_mode="normal",
|
||
active_thinking_mode="normal",
|
||
deep_thinking=False,
|
||
forced_deep_reason="",
|
||
deep_retrieval_steps=2,
|
||
deep_vector_top_k=3,
|
||
system_snapshot={},
|
||
user_snapshot=None,
|
||
started=time.perf_counter(),
|
||
message_intent=None,
|
||
mixed_signal_part=None,
|
||
answer_mode="retrieval_normal",
|
||
consultant_mode_preference=None,
|
||
route_decision={
|
||
"route_mode": "external",
|
||
"effective_source_policy": "external",
|
||
"decision_origin": "query_semantics_contract",
|
||
"reply_contract": {"head": "外部实时查询:", "tail_hint": None},
|
||
"query_profile": {"needs_freshness": True, "referenced_objects": ["public_api"]},
|
||
},
|
||
)
|
||
|
||
assert service.fit_brief_seen == {}
|
||
assert result["fit_brief"] == {}
|
||
assert result["external_recommendation"] == {}
|
||
|
||
|
||
def test_external_recommendation_does_not_treat_capability_name_as_direct_match_without_formal_anchor() -> None:
|
||
result = AdaptiveRAGAnswerPayloadMixin._build_external_recommendation(
|
||
external_context_packet={
|
||
"retrieval_status": "evidence_found",
|
||
"verified_evidence": [
|
||
{
|
||
"source_type": "url",
|
||
"source_label": "OpenAI Docs",
|
||
"title": "AI 管家接入 Responses API",
|
||
"target_url": "https://platform.openai.com/docs/guides/responses",
|
||
"snippet": "AI 管家可以通过 Responses API 编排工具调用。",
|
||
}
|
||
],
|
||
"verifier": {"ok": True},
|
||
},
|
||
fit_brief={
|
||
"project_summary": "当前项目:PMFlow AI 管家",
|
||
"current_capabilities": ["AI 管家"],
|
||
"signal_source_anchors": [],
|
||
"constraints": [],
|
||
"recommendation_focus": [],
|
||
},
|
||
)
|
||
|
||
assert result["retrieval_status"] == "evidence_found"
|
||
assert result["impact_status"] == "indirect_only"
|
||
assert result["affected_signal_sources"] == []
|
||
|
||
|
||
def test_external_recommendation_uses_formal_signal_source_anchor_for_direct_match() -> None:
|
||
result = AdaptiveRAGAnswerPayloadMixin._build_external_recommendation(
|
||
external_context_packet={
|
||
"retrieval_status": "evidence_found",
|
||
"verified_evidence": [
|
||
{
|
||
"source_type": "url",
|
||
"source_label": "OpenAI Docs",
|
||
"title": "Responses API guide",
|
||
"target_url": "https://platform.openai.com/docs/guides/responses",
|
||
"snippet": "OpenAI Docs 说明 Responses API 支持结构化输出与工具调用。",
|
||
}
|
||
],
|
||
"verifier": {"ok": True},
|
||
},
|
||
fit_brief={
|
||
"project_summary": "当前项目:PMFlow AI 管家",
|
||
"current_capabilities": ["AI 管家"],
|
||
"signal_source_anchors": ["OpenAI Docs"],
|
||
"constraints": [],
|
||
"recommendation_focus": [],
|
||
},
|
||
)
|
||
|
||
assert result["retrieval_status"] == "evidence_found"
|
||
assert result["impact_status"] == "direct_match"
|
||
assert result["affected_signal_sources"] == ["OpenAI Docs"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_deep_guard_answer_prefers_member_workload_direct_answer() -> None:
|
||
class _FakeResult:
|
||
def __init__(self, rows=None, scalar=None):
|
||
self._rows = rows or []
|
||
self._scalar = scalar
|
||
|
||
def all(self):
|
||
return self._rows
|
||
|
||
def first(self):
|
||
return self._rows[0] if self._rows else None
|
||
|
||
def scalar_one_or_none(self):
|
||
return self._scalar
|
||
|
||
class _FakeDB:
|
||
def __init__(self) -> None:
|
||
self.calls = 0
|
||
|
||
async def execute(self, stmt, params=None): # noqa: ANN001
|
||
self.calls += 1
|
||
if self.calls == 1:
|
||
return _FakeResult(rows=[SimpleNamespace(id=24, name="杨宇", username="yangyu")])
|
||
return _FakeResult(rows=[(265.56, 95.6, 60.0)])
|
||
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError("LLM should not be called for direct member workload answer")
|
||
|
||
class _FakeService(AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = _FakeDB()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
def _normalize_thinking_mode(self, thinking_mode: str) -> str:
|
||
return "normal"
|
||
|
||
async def get_project_manifest(self, project_id):
|
||
_ = project_id
|
||
return {}
|
||
|
||
async def _resolve_answer_scene_semantics(self, **kwargs):
|
||
_ = kwargs
|
||
return {
|
||
"scene_key": "member_workload",
|
||
"snapshot_focus": "none",
|
||
"deep_intent": "none",
|
||
"confidence": 0.96,
|
||
"reason": "structured-scene",
|
||
}
|
||
|
||
async def _select_scene_prompt(self, **kwargs):
|
||
_ = kwargs
|
||
return "member_workload", "member workload prompt"
|
||
|
||
def _enforce_feature_evidence_in_answer(self, **kwargs):
|
||
return str(kwargs.get("answer_text") or "")
|
||
|
||
service = _FakeService()
|
||
result = await service._generate_answer(
|
||
question="杨宇现在负载情况如何?",
|
||
retrieved=[],
|
||
conversation=[],
|
||
project_id=None,
|
||
asked_via="feishu",
|
||
)
|
||
|
||
assert "杨宇当前负载265.56%" in result
|
||
assert "严重超载" in result
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_deep_guard_answer_prefers_structured_member_workload_scene_without_keyword_hints() -> None:
|
||
class _FakeResult:
|
||
def __init__(self, rows=None):
|
||
self._rows = rows or []
|
||
|
||
def all(self):
|
||
return self._rows
|
||
|
||
def first(self):
|
||
return self._rows[0] if self._rows else None
|
||
|
||
class _FakeDB:
|
||
def __init__(self) -> None:
|
||
self.calls = 0
|
||
|
||
async def execute(self, stmt, params=None): # noqa: ANN001
|
||
_ = (stmt, params)
|
||
self.calls += 1
|
||
if self.calls == 1:
|
||
return _FakeResult(rows=[SimpleNamespace(id=24, name="杨宇", username="yangyu")])
|
||
return _FakeResult(rows=[(82.0, 41.0, 50.0)])
|
||
|
||
class _FakeService(AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = _FakeDB()
|
||
self.tenant_id = 1
|
||
|
||
def _normalize_thinking_mode(self, thinking_mode: str) -> str:
|
||
_ = thinking_mode
|
||
return "normal"
|
||
|
||
async def get_project_manifest(self, project_id):
|
||
_ = project_id
|
||
return {}
|
||
|
||
async def _resolve_answer_scene_semantics(self, **kwargs):
|
||
_ = kwargs
|
||
return {
|
||
"scene_key": "member_workload",
|
||
"snapshot_focus": "none",
|
||
"deep_intent": "none",
|
||
"confidence": 0.95,
|
||
"reason": "structured-scene",
|
||
}
|
||
|
||
async def _select_scene_prompt(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError("scene prompt should not be rendered for direct workload answer")
|
||
|
||
def _enforce_feature_evidence_in_answer(self, **kwargs):
|
||
return str(kwargs.get("answer_text") or "")
|
||
|
||
service = _FakeService()
|
||
result = await service._generate_answer(
|
||
question="杨宇现在忙不忙?",
|
||
retrieved=[],
|
||
conversation=[],
|
||
project_id=None,
|
||
asked_via="feishu",
|
||
)
|
||
|
||
assert "杨宇当前负载82.00%" in result
|
||
assert "负载偏高" in result
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_structured_db_retrieval_prefers_structured_people_focus_without_keyword_hints() -> None:
|
||
class _FakeScalarResult:
|
||
def __init__(self, rows):
|
||
self._rows = rows
|
||
|
||
def scalars(self):
|
||
return self
|
||
|
||
def all(self):
|
||
return self._rows
|
||
|
||
class _FakeDB:
|
||
async def execute(self, stmt, params=None): # noqa: ANN001
|
||
_ = (stmt, params)
|
||
return _FakeScalarResult(
|
||
[
|
||
SimpleNamespace(id=7, name="杨宇", role="backend", updated_at=None),
|
||
SimpleNamespace(id=8, name="林宇航", role="pm", updated_at=None),
|
||
]
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGRetrievalRouterDBMixin):
|
||
def __init__(self) -> None:
|
||
self.db = _FakeDB()
|
||
self.tenant_id = 1
|
||
|
||
def _query_subject_focus(self, query_semantics): # noqa: ANN001
|
||
return str((query_semantics or {}).get("subject_focus") or "unknown")
|
||
|
||
def _query_scope_hint(self, query_semantics): # noqa: ANN001
|
||
return str((query_semantics or {}).get("scope_hint") or "none")
|
||
|
||
def _normalize_source_registry_metadata(self, *, source_type: str, metadata: dict, explicit_updated_at=None):
|
||
_ = (source_type, explicit_updated_at)
|
||
return dict(metadata)
|
||
|
||
service = _FakeService()
|
||
rows = await service._retrieve_structured_db(
|
||
"他们都有哪些人?",
|
||
project_id=None,
|
||
query_semantics={
|
||
"subject_focus": "people",
|
||
"scope_hint": "global",
|
||
"feature_focus": "none",
|
||
},
|
||
)
|
||
|
||
assert len(rows) == 1
|
||
assert "成员清单" in rows[0].content
|
||
assert "杨宇" in rows[0].content
|
||
|
||
|
||
def test_feature_evidence_prefers_structured_scene_without_keyword_hints() -> None:
|
||
class _FakeService(AdaptiveRAGManifestFeatureMixinP2):
|
||
def _query_subject_focus(self, query_semantics): # noqa: ANN001
|
||
return str((query_semantics or {}).get("subject_focus") or "unknown")
|
||
|
||
def _query_feature_focus(self, query_semantics): # noqa: ANN001
|
||
return str((query_semantics or {}).get("feature_focus") or "none")
|
||
|
||
def _collect_feature_evidence(self, *, question: str, retrieved, system_snapshot):
|
||
_ = (question, retrieved, system_snapshot)
|
||
return [
|
||
{
|
||
"feature_name": "智能配置",
|
||
"route": "/ai-config",
|
||
"page_file": "frontend/src/pages/AIConfig.tsx",
|
||
"apis": ["/api/v1/ai-config"],
|
||
}
|
||
]
|
||
|
||
service = _FakeService()
|
||
text = service._enforce_feature_evidence_in_answer(
|
||
question="这个模块怎么进?",
|
||
answer_text="先看说明。",
|
||
retrieved=[],
|
||
system_snapshot={},
|
||
scene_semantics={"snapshot_focus": "feature"},
|
||
)
|
||
|
||
assert "证据补充(Feature Catalog)" in text
|
||
assert "/ai-config" in text
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_deep_guard_answer_feishu_direct_prefers_manifest_and_compact_focus_context() -> None:
|
||
class _FakeLLM:
|
||
def __init__(self) -> None:
|
||
self.prompt = ""
|
||
|
||
async def generate(self, prompt: str, **kwargs):
|
||
_ = kwargs
|
||
self.prompt = prompt
|
||
return "信息不足,缺少:目标对象"
|
||
|
||
class _FakeService(AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
def _normalize_thinking_mode(self, thinking_mode: str) -> str:
|
||
return "normal"
|
||
|
||
async def get_project_manifest(self, project_id):
|
||
_ = project_id
|
||
return {
|
||
"manifest_text": "项目#7 Alpha(进行中);功能 18 个;KG 实体 12,关系 19。"
|
||
}
|
||
|
||
async def _render_scene_prompt(self, *, prompt_key: str, fallback_template: str, **kwargs):
|
||
_ = prompt_key
|
||
return str(fallback_template).format(**kwargs).strip()
|
||
|
||
def _enforce_feature_evidence_in_answer(self, **kwargs):
|
||
return str(kwargs.get("answer_text") or "")
|
||
|
||
service = _FakeService()
|
||
system_snapshot = {
|
||
"assistant_profile": {"name": "PMFlow", "identity": "项目管理顾问"},
|
||
"platform_profile": {
|
||
"backend": {"module_counts": {"projects": 28, "tasks": 146, "signals": 41}},
|
||
"feature_catalog": [
|
||
{"name": "项目管理", "route": "/projects"},
|
||
{"name": "信号接入", "route": "/signals"},
|
||
{"name": "任务看板", "route": "/board"},
|
||
{"name": "知识图谱", "route": "/kg"},
|
||
{"name": "训练中心", "route": "/training"},
|
||
],
|
||
},
|
||
}
|
||
user_snapshot = {
|
||
"current_user": {"name": "林宇航"},
|
||
"projects": [{"id": idx, "title": f"项目{idx}"} for idx in range(1, 15)],
|
||
"tasks": [{"id": idx, "title": f"任务{idx}"} for idx in range(1, 10)],
|
||
}
|
||
|
||
result = await service._generate_answer(
|
||
question="信息不全,继续提供",
|
||
retrieved=[],
|
||
conversation=[
|
||
{"question": "上一次你缺什么", "answer": "缺目标对象和验收口径"},
|
||
{"question": "现在呢", "answer": "还缺时间边界"},
|
||
],
|
||
project_id=7,
|
||
system_snapshot=system_snapshot,
|
||
user_snapshot=user_snapshot,
|
||
asked_via="feishu",
|
||
)
|
||
|
||
assert result == "信息不足,缺少:目标对象"
|
||
assert "项目画像:" in service.llm.prompt
|
||
assert "项目#7 Alpha(进行中)" in service.llm.prompt
|
||
assert "系统摘要:" in service.llm.prompt
|
||
assert "用户焦点:" in service.llm.prompt
|
||
assert "林宇航" in service.llm.prompt
|
||
assert '"projects"' not in service.llm.prompt
|
||
assert "项目14" not in service.llm.prompt
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_select_scene_prompt_applies_consultant_mode_preference_wrapper() -> None:
|
||
class _FakeService(AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
async def _render_scene_prompt(self, *, prompt_key: str, fallback_template: str, **kwargs):
|
||
_ = (fallback_template, kwargs)
|
||
return f"PROMPT::{prompt_key}"
|
||
|
||
service = _FakeService()
|
||
|
||
scene_key, prompt = await service._select_scene_prompt(
|
||
question="这个方案怎么做更稳妥?",
|
||
project_id=7,
|
||
asked_via="pmflow",
|
||
message_intent=None,
|
||
mixed_signal_part=None,
|
||
conversation_text="无",
|
||
context_blocks_text="无",
|
||
system_snapshot={},
|
||
user_snapshot={},
|
||
system_snapshot_text="{}",
|
||
user_snapshot_text="{}",
|
||
relevance_max=0.61,
|
||
query_type="explain",
|
||
consultant_mode_preference="advise",
|
||
)
|
||
|
||
assert scene_key == "default_consultant_advise"
|
||
assert "PROMPT::PROMPT_RAG_ANSWER" in prompt
|
||
assert "稳妥建议" in prompt
|
||
|
||
|
||
def test_ai_pmo_qa_prompt_includes_five_lenses_and_runtime_state_guard() -> None:
|
||
assert "事实源在哪" in PROMPT_AI_PMO_QA
|
||
assert "输出模板在哪" in PROMPT_AI_PMO_QA
|
||
assert "检索范围在哪" in PROMPT_AI_PMO_QA
|
||
assert "执行器在哪" in PROMPT_AI_PMO_QA
|
||
assert "反馈回流在哪" in PROMPT_AI_PMO_QA
|
||
assert "静态资料" in PROMPT_AI_PMO_QA
|
||
assert "运行态配置" in PROMPT_AI_PMO_QA
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_select_scene_prompt_routes_business_review_to_ai_pmo_prompt() -> None:
|
||
class _FakeService(AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
async def _render_scene_prompt(self, *, prompt_key: str, fallback_template: str, **kwargs):
|
||
_ = fallback_template
|
||
return (
|
||
f"PROMPT::{prompt_key}|"
|
||
f"business_review={kwargs.get('business_review_mode')}|"
|
||
f"framework={kwargs.get('review_framework')}"
|
||
)
|
||
|
||
service = _FakeService()
|
||
|
||
scene_key, prompt = await service._select_scene_prompt(
|
||
question="我要做一个大模型 API 聚合平台,帮我分析下要怎么做",
|
||
project_id=None,
|
||
asked_via="feishu",
|
||
message_intent="business_review",
|
||
mixed_signal_part=None,
|
||
conversation_text="用户上一轮提到要做 API 聚合平台",
|
||
context_blocks_text="无",
|
||
system_snapshot={},
|
||
user_snapshot={},
|
||
system_snapshot_text="{}",
|
||
user_snapshot_text="{}",
|
||
relevance_max=0.51,
|
||
query_type="explain",
|
||
consultant_mode_preference="clarify",
|
||
)
|
||
|
||
assert scene_key == "ai_pmo_business_review_consultant_clarify"
|
||
assert "PROMPT::PROMPT_AI_PMO_QA" in prompt
|
||
assert "business_review=true" in prompt
|
||
assert "framework=five_lenses" in prompt
|
||
|
||
|
||
def test_runtime_state_query_guard_blocks_static_catalog_evidence() -> None:
|
||
class _FakeService(AdaptiveRAGDeepGuardRulesMixin):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
decision = service._evaluate_post_retrieval_guard(
|
||
deep_intent="runtime_state_query",
|
||
sources=[
|
||
{"source": "feature_catalog", "label": "Feature Catalog", "metadata": {}},
|
||
{"source": "graph", "label": "Knowledge Graph", "metadata": {"path": "1->uses->2"}},
|
||
],
|
||
relevance_max=0.96,
|
||
)
|
||
|
||
assert decision["should_block_with_guard"] is True
|
||
assert decision["guard_focus"] == "runtime_state"
|
||
assert "无法仅凭静态资料确认" in service._build_evidence_guard_answer(
|
||
"高级模型路由是否启用?",
|
||
guard_focus=str(decision["guard_focus"]),
|
||
)
|
||
|
||
|
||
def test_runtime_state_query_guard_allows_routing_config_evidence() -> None:
|
||
class _FakeService(AdaptiveRAGDeepGuardRulesMixin):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
decision = service._evaluate_post_retrieval_guard(
|
||
deep_intent="runtime_state_query",
|
||
sources=[
|
||
{
|
||
"source": "doc",
|
||
"label": "Routing Config",
|
||
"metadata": {"source_id": "backend/.control_plane/pmflow-routing-rules.json"},
|
||
}
|
||
],
|
||
relevance_max=0.74,
|
||
)
|
||
|
||
assert decision["should_block_with_guard"] is False
|
||
|
||
|
||
def test_engine_process_guard_requests_auto_retry_on_snapshot_only_sources() -> None:
|
||
class _FakeService(AdaptiveRAGDeepGuardRulesMixin):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
decision = service._evaluate_post_retrieval_guard(
|
||
deep_intent="engine_process_query",
|
||
sources=[
|
||
{"source": "system", "label": "System Snapshot", "metadata": {}},
|
||
{"source": "system", "label": "User Snapshot", "metadata": {}},
|
||
],
|
||
relevance_max=0.41,
|
||
)
|
||
|
||
assert decision["auto_retry_reason"] == "snapshot_only"
|
||
|
||
|
||
def test_chat_external_fact_scene_policy_preserves_anchored_external_scene_without_question_heuristics() -> None:
|
||
class _FakeService(AdaptiveRAGAnswerMainlineMixin):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
normalized = service._apply_chat_external_fact_scene_policy(
|
||
question="不对吧,截止当前日期都已经有 codex5.4 了",
|
||
project_id=None,
|
||
scene_semantics={
|
||
"scene_key": "ai_pmo_correction",
|
||
"snapshot_focus": "feature",
|
||
"deep_intent": "correction_turn",
|
||
"confidence": 0.95,
|
||
},
|
||
anchor_prompt_variant="external_trend",
|
||
)
|
||
|
||
assert normalized["scene_key"] == "industry_trend"
|
||
assert normalized["deep_intent"] == "none"
|
||
assert normalized["external_fact_chat"] is True
|
||
|
||
|
||
def test_chat_external_fact_scene_policy_keeps_existing_external_scene() -> None:
|
||
class _FakeService(AdaptiveRAGAnswerMainlineMixin):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
normalized = service._apply_chat_external_fact_scene_policy(
|
||
question="当前codex最新的模型用的是什么?",
|
||
project_id=None,
|
||
scene_semantics={
|
||
"scene_key": "industry_trend",
|
||
"snapshot_focus": "none",
|
||
"deep_intent": "none",
|
||
"confidence": 0.92,
|
||
},
|
||
anchor_prompt_variant=None,
|
||
)
|
||
|
||
assert normalized["scene_key"] == "industry_trend"
|
||
assert normalized["snapshot_focus"] == "none"
|
||
assert normalized["external_fact_chat"] is True
|
||
|
||
|
||
def test_chat_external_fact_scene_policy_keeps_non_external_scene_unchanged() -> None:
|
||
class _FakeService(AdaptiveRAGAnswerMainlineMixin):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
normalized = service._apply_chat_external_fact_scene_policy(
|
||
question="我们系统当前接的是哪个 codex 模型配置?",
|
||
project_id=None,
|
||
scene_semantics={
|
||
"scene_key": "ai_pmo",
|
||
"snapshot_focus": "identity",
|
||
"deep_intent": "none",
|
||
"confidence": 0.92,
|
||
},
|
||
anchor_prompt_variant=None,
|
||
)
|
||
|
||
assert normalized["scene_key"] == "ai_pmo"
|
||
assert normalized["snapshot_focus"] == "identity"
|
||
assert normalized.get("external_fact_chat") is not True
|
||
|
||
|
||
def test_external_fact_freshness_guard_rejects_internal_only_sources() -> None:
|
||
class _FakeService(AdaptiveRAGDeepGuardRulesMixin):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
assert service._requires_external_freshness(
|
||
question="codex 目前最高模型出到什么了",
|
||
scene_semantics={"scene_key": "industry_trend", "requires_freshness": True},
|
||
) is True
|
||
assert service._has_external_grounding_sources(
|
||
[
|
||
{"source": "graph", "label": "Knowledge Graph", "metadata": {"path": "1->belongs_to->2"}},
|
||
{"source": "feature_catalog", "label": "Feature Catalog", "metadata": {}},
|
||
]
|
||
) is False
|
||
answer = service._build_external_freshness_guard_answer("codex 目前最高模型出到什么了")
|
||
assert "当前未接入实时外部数据" in answer
|
||
assert "backend/app/services" not in answer
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_second_pass_query_skips_internal_hints_for_external_freshness_only() -> None:
|
||
class _FakeService(AdaptiveRAGMultiStepCoreMixin):
|
||
async def entity_resolution(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"entity_resolution should be skipped: {kwargs}")
|
||
|
||
async def get_project_manifest(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"get_project_manifest should be skipped: {kwargs}")
|
||
|
||
service = _FakeService()
|
||
revised, meta = await service._build_second_pass_query(
|
||
question="codex 目前最高模型出到什么了",
|
||
project_id=None,
|
||
query_semantics={"external_freshness_only": True},
|
||
)
|
||
|
||
assert revised == "codex 目前最高模型出到什么了"
|
||
assert meta["mode"] == "freshness_guard_skip_internal_hint"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_live_external_freshness_context_fetches_url_rows_before_guard() -> None:
|
||
class _FakeService(AdaptiveRAGAnswerRetrievalMixin, AdaptiveRAGDeepGuardRulesMixin):
|
||
async def _retrieve_live_external_context(self, *, question: str, max_results: int = 3, max_pages: int = 3):
|
||
_ = (question, max_results, max_pages)
|
||
return (
|
||
[
|
||
RetrievedItem(
|
||
source="url",
|
||
content="OpenAI 在官方文章中说明 GPT-5.4 正在 rollout 到 Codex。",
|
||
score=0.93,
|
||
source_label="URL[Introducing GPT-5.4]",
|
||
metadata={
|
||
"url": "https://openai.com/index/introducing-gpt-5-4/",
|
||
"target_url": "https://openai.com/index/introducing-gpt-5-4/",
|
||
"title": "Introducing GPT-5.4",
|
||
},
|
||
)
|
||
],
|
||
{"query": question, "result_count": 1, "fetched_pages": 1},
|
||
)
|
||
|
||
service = _FakeService()
|
||
rows, meta = await service._maybe_fetch_live_external_freshness_context(
|
||
question="当前codex最新的模型用的是什么?",
|
||
effective_question="当前codex最新的模型用的是什么?",
|
||
query_semantics={},
|
||
scene_semantics={"scene_key": "industry_trend", "requires_freshness": True},
|
||
)
|
||
|
||
assert len(rows) == 1
|
||
assert rows[0].source == "url"
|
||
assert rows[0].metadata["target_url"] == "https://openai.com/index/introducing-gpt-5-4/"
|
||
assert meta["mode"] == "live_external_context"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_live_external_freshness_context_prefers_external_query_gateway() -> None:
|
||
class _FakeService(AdaptiveRAGAnswerRetrievalMixin, AdaptiveRAGDeepGuardRulesMixin):
|
||
async def _execute_external_query_gateway(
|
||
self,
|
||
*,
|
||
question: str,
|
||
query_semantics: dict | None = None,
|
||
result_limit: int = 3,
|
||
max_pages: int = 3,
|
||
):
|
||
_ = (question, query_semantics, result_limit, max_pages)
|
||
return (
|
||
[
|
||
RetrievedItem(
|
||
source="url",
|
||
content="OpenAI 官方页面显示 Codex 最新模型是 GPT-5.4。",
|
||
score=0.94,
|
||
source_label="URL[Models - Codex | OpenAI Developers]",
|
||
metadata={
|
||
"url": "https://developers.openai.com/codex/models",
|
||
"target_url": "https://developers.openai.com/codex/models",
|
||
"title": "Models - Codex | OpenAI Developers",
|
||
},
|
||
)
|
||
],
|
||
{
|
||
"mode": "external_query_gateway",
|
||
"adapter": "live_search",
|
||
"search_result_count": 1,
|
||
"fetched_pages": 1,
|
||
"query": "codex latest model",
|
||
},
|
||
)
|
||
|
||
service = _FakeService()
|
||
rows, meta = await service._maybe_fetch_live_external_freshness_context(
|
||
question="当前codex最新的模型用的是什么?",
|
||
effective_question="当前codex最新的模型用的是什么?",
|
||
query_semantics={},
|
||
scene_semantics={"scene_key": "industry_trend", "requires_freshness": True},
|
||
)
|
||
|
||
assert len(rows) == 1
|
||
assert rows[0].metadata["target_url"] == "https://developers.openai.com/codex/models"
|
||
assert meta["mode"] == "external_query_gateway"
|
||
assert meta["adapter"] == "live_search"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_external_query_gateway_falls_back_to_live_search_when_mcp_has_no_evidence() -> None:
|
||
from app.services.external_query_service.gateway import ExternalQueryGateway
|
||
from app.services.external_query_service.registry import ExternalProviderRegistry
|
||
from app.services.external_query_service.schemas import (
|
||
ExternalAdapterResult,
|
||
ExternalEvidenceItem,
|
||
ExternalQueryPlan,
|
||
)
|
||
|
||
plan = ExternalQueryPlan(
|
||
plan_version="v1",
|
||
query_kind="model",
|
||
answer_contract="summary_with_sources",
|
||
freshness_required=True,
|
||
topic="codex 最新模型",
|
||
subject_entities=["Codex"],
|
||
location_hint=None,
|
||
time_range_hint="截至今天",
|
||
preferred_domains=["openai.com"],
|
||
search_queries=["codex latest model"],
|
||
result_limit=3,
|
||
requires_multi_source=False,
|
||
clarify_on_ambiguity=False,
|
||
)
|
||
|
||
class _Planner:
|
||
async def build_plan(self, *, question: str, query_semantics: dict | None = None) -> ExternalQueryPlan:
|
||
_ = (question, query_semantics)
|
||
return plan
|
||
|
||
class _MCPAdapter:
|
||
adapter_name = "mcp"
|
||
|
||
async def execute(self, plan: ExternalQueryPlan) -> ExternalAdapterResult:
|
||
_ = plan
|
||
return ExternalAdapterResult(adapter_name="mcp", status="unavailable", evidence=[], meta={"reason": "no_client"})
|
||
|
||
class _LiveSearchAdapter:
|
||
adapter_name = "live_search"
|
||
|
||
async def execute(self, plan: ExternalQueryPlan) -> ExternalAdapterResult:
|
||
_ = plan
|
||
return ExternalAdapterResult(
|
||
adapter_name="live_search",
|
||
status="success",
|
||
evidence=[
|
||
ExternalEvidenceItem(
|
||
source_type="url",
|
||
source_label="OpenAI Developers",
|
||
title="Models - Codex | OpenAI Developers",
|
||
target_url="https://developers.openai.com/codex/models",
|
||
snippet="Latest: GPT-5.4",
|
||
retrieved_at="2026-04-15T10:00:00+08:00",
|
||
freshness_score=0.98,
|
||
authority_score=0.96,
|
||
grounded_fields=["latest_model"],
|
||
raw_metadata={"provider": "live_search"},
|
||
)
|
||
],
|
||
meta={"provider": "live_search"},
|
||
)
|
||
|
||
gateway = ExternalQueryGateway(
|
||
planner=_Planner(),
|
||
registry=ExternalProviderRegistry(adapters=[_MCPAdapter(), _LiveSearchAdapter()]),
|
||
)
|
||
|
||
result = await gateway.execute(
|
||
question="当前 codex 最新的模型用的是什么?",
|
||
query_semantics={"query_profile": {"needs_freshness": True, "referenced_objects": ["public_model"]}},
|
||
)
|
||
|
||
assert result.adapter_name == "live_search"
|
||
assert result.verifier["ok"] is True
|
||
assert len(result.evidence) == 1
|
||
assert result.meta["fallback_used"] is True
|
||
|
||
|
||
def test_external_route_source_filter_is_fail_closed() -> None:
|
||
class _FakeService(AdaptiveRAGQueryRouteGatewayMixin):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
filtered = service._filter_sources_for_route(
|
||
sources=[
|
||
{"source": "feishu", "label": "Feishu history", "score": 0.8},
|
||
{"source": "pmflow", "label": "PMFlow records", "score": 0.7},
|
||
],
|
||
route_decision={"effective_source_policy": "external"},
|
||
)
|
||
|
||
assert filtered == []
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_live_external_search_uses_proxy_aware_http_client(monkeypatch) -> None:
|
||
captured: dict[str, object] = {}
|
||
|
||
class _FakeResponse:
|
||
def __init__(self, text: str) -> None:
|
||
self.text = text
|
||
|
||
def raise_for_status(self) -> None:
|
||
return None
|
||
|
||
class _FakeAsyncClient:
|
||
def __init__(self, *args, **kwargs) -> None:
|
||
_ = args
|
||
captured.update(kwargs)
|
||
|
||
async def __aenter__(self):
|
||
return self
|
||
|
||
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||
_ = (exc_type, exc, tb)
|
||
return None
|
||
|
||
async def get(self, url: str, headers: dict[str, str]):
|
||
captured["url"] = url
|
||
captured["headers"] = headers
|
||
return _FakeResponse('<a class="result__a" href="https://example.com/post">OpenAI Codex latest model</a>')
|
||
|
||
monkeypatch.setattr(
|
||
"app.services.adaptive_rag_service.mixin_conversation_utils_a_p1.httpx.AsyncClient",
|
||
_FakeAsyncClient,
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGConversationUtilsAMixinP1):
|
||
pass
|
||
|
||
rows = await _FakeService()._search_live_external_results(
|
||
query="OpenAI Codex latest model",
|
||
max_results=1,
|
||
)
|
||
|
||
assert captured["trust_env"] is True
|
||
assert rows == [{"url": "https://example.com/post", "title": "OpenAI Codex latest model"}]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_live_external_fetch_uses_proxy_aware_http_client(monkeypatch) -> None:
|
||
captured: dict[str, object] = {}
|
||
|
||
class _FakeResponse:
|
||
def __init__(self) -> None:
|
||
self.headers = {"content-type": "text/html"}
|
||
self.text = "<html><body><h1>Fresh external fact</h1></body></html>"
|
||
|
||
def raise_for_status(self) -> None:
|
||
return None
|
||
|
||
class _FakeAsyncClient:
|
||
def __init__(self, *args, **kwargs) -> None:
|
||
_ = args
|
||
captured.update(kwargs)
|
||
|
||
async def __aenter__(self):
|
||
return self
|
||
|
||
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||
_ = (exc_type, exc, tb)
|
||
return None
|
||
|
||
async def get(self, url: str, headers: dict[str, str]):
|
||
captured["url"] = url
|
||
captured["headers"] = headers
|
||
return _FakeResponse()
|
||
|
||
monkeypatch.setattr(
|
||
"app.services.adaptive_rag_service.mixin_conversation_utils_a_p1.httpx.AsyncClient",
|
||
_FakeAsyncClient,
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGConversationUtilsAMixinP1):
|
||
pass
|
||
|
||
text = await _FakeService()._fetch_url_text("https://example.com/post")
|
||
|
||
assert captured["trust_env"] is True
|
||
assert captured["url"] == "https://example.com/post"
|
||
assert "Fresh external fact" in text
|
||
|
||
|
||
def test_live_external_search_result_score_prefers_official_domain_for_model_brand_query() -> None:
|
||
official = AdaptiveRAGConversationUtilsAMixinP1._score_live_external_search_result(
|
||
question="当前 codex 最新的模型用的是什么?",
|
||
title="Codex 全面升级 - OpenAI",
|
||
url="https://openai.com/zh-Hans-CN/index/introducing-upgrades-to-codex/",
|
||
)
|
||
third_party = AdaptiveRAGConversationUtilsAMixinP1._score_live_external_search_result(
|
||
question="当前 codex 最新的模型用的是什么?",
|
||
title="Codex 模型选择 - 菜鸟教程",
|
||
url="https://www.runoob.com/codex/codex-models.html",
|
||
)
|
||
|
||
assert official > third_party
|
||
|
||
|
||
def test_live_external_search_result_score_prefers_codex_models_reference_page_for_latest_model_question() -> None:
|
||
reference_page = AdaptiveRAGConversationUtilsAMixinP1._score_live_external_search_result(
|
||
question="当前 codex 最新的模型用的是什么?",
|
||
title="Models - Codex | OpenAI Developers",
|
||
url="https://developers.openai.com/codex/models",
|
||
)
|
||
announcement_page = AdaptiveRAGConversationUtilsAMixinP1._score_live_external_search_result(
|
||
question="当前 codex 最新的模型用的是什么?",
|
||
title="Introducing GPT-5.3-Codex - OpenAI",
|
||
url="https://openai.com/index/introducing-gpt-5-3-codex/",
|
||
)
|
||
|
||
assert reference_page > announcement_page
|
||
|
||
|
||
def test_live_external_search_queries_prefer_official_site_for_model_brand_question() -> None:
|
||
queries = AdaptiveRAGConversationUtilsAMixinP1._build_live_external_search_queries(
|
||
"当前 codex 最新的模型用的是什么?"
|
||
)
|
||
|
||
assert queries
|
||
assert queries[0] == "site:openai.com codex latest model"
|
||
assert any(query == "当前 codex 最新的模型用的是什么" for query in queries)
|
||
|
||
|
||
def test_live_external_search_queries_strip_internal_runtime_tokens_for_model_brand_question() -> None:
|
||
queries = AdaptiveRAGConversationUtilsAMixinP1._build_live_external_search_queries(
|
||
"围绕“_sync_pmflow_rag_async->backend/app/api/sstc.py”回答:当前codex最新的模型用的是什么?"
|
||
)
|
||
|
||
assert queries
|
||
assert queries[0] == "site:openai.com codex latest model"
|
||
assert all("backend/app/api/sstc.py" not in query for query in queries)
|
||
assert all("_sync_pmflow_rag_async" not in query for query in queries)
|
||
|
||
|
||
def test_live_external_chunk_score_prefers_latest_fact_chunk_for_chinese_model_question() -> None:
|
||
latest_chunk = (
|
||
"Models – Codex | OpenAI Developers Latest: GPT-5.4 "
|
||
"Prompt guidance Core concepts Text generation Code generation"
|
||
)
|
||
nav_chunk = (
|
||
"Codex Docs Guides, concepts, and product docs for Codex "
|
||
"Use cases Example workflows and tasks teams hand to Codex"
|
||
)
|
||
|
||
latest_score = AdaptiveRAGConversationUtilsAMixinP1._score_live_external_chunk(
|
||
question="当前codex最新的模型用的是什么?",
|
||
title="Models - Codex | OpenAI Developers",
|
||
content=latest_chunk,
|
||
url="https://developers.openai.com/codex/models",
|
||
)
|
||
nav_score = AdaptiveRAGConversationUtilsAMixinP1._score_live_external_chunk(
|
||
question="当前codex最新的模型用的是什么?",
|
||
title="Models - Codex | OpenAI Developers",
|
||
content=nav_chunk,
|
||
url="https://developers.openai.com/codex/models",
|
||
)
|
||
|
||
assert latest_score > nav_score
|
||
|
||
|
||
def test_external_model_fact_direct_answer_prefers_explicit_latest_evidence() -> None:
|
||
retrieved = [
|
||
RetrievedItem(
|
||
source="url",
|
||
content=(
|
||
"Models – Codex | OpenAI Developers Latest: GPT-5.4 "
|
||
"Prompt guidance Core concepts Text generation"
|
||
),
|
||
score=0.94,
|
||
source_label="URL[Models - Codex | OpenAI Developers]",
|
||
metadata={
|
||
"url": "https://developers.openai.com/codex/models",
|
||
"target_url": "https://developers.openai.com/codex/models",
|
||
},
|
||
)
|
||
]
|
||
|
||
answer = AdaptiveRAGDeepGuardAnswerMixin._build_external_model_fact_direct_answer(
|
||
question="当前codex最新的模型用的是什么?",
|
||
retrieved=retrieved,
|
||
scene_semantics={"scene_key": "industry_trend"},
|
||
)
|
||
|
||
assert answer is not None
|
||
assert "GPT-5.4" in answer
|
||
assert "OpenAI" in answer
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_live_external_context_prefers_official_candidate_beyond_base_page_window() -> None:
|
||
class _FakeService(AdaptiveRAGConversationUtilsAMixinP1):
|
||
async def _search_live_external_results(self, *, query: str, max_results: int = 5):
|
||
_ = (query, max_results)
|
||
return [
|
||
{"url": "https://example.com/codex-guide", "title": "Codex 全面上手指南"},
|
||
{"url": "https://example.com/codex-models", "title": "Codex 模型选择"},
|
||
{"url": "https://example.com/codex-news", "title": "Codex 最新动态"},
|
||
{
|
||
"url": "https://openai.com/zh-Hans-CN/index/introducing-upgrades-to-codex/",
|
||
"title": "Codex 全面升级 - OpenAI",
|
||
},
|
||
]
|
||
|
||
async def _fetch_url_text(self, url: str) -> str:
|
||
if "openai.com" in url:
|
||
return "OpenAI 官方说明 Codex 当前使用 GPT-5.4。"
|
||
return "第三方教程,只介绍 Codex 的一般用法。"
|
||
|
||
rows, meta = await _FakeService()._retrieve_live_external_context(
|
||
question="当前 codex 最新的模型用的是什么?",
|
||
max_results=1,
|
||
max_pages=1,
|
||
)
|
||
|
||
assert rows
|
||
assert rows[0].metadata["url"] == "https://openai.com/zh-Hans-CN/index/introducing-upgrades-to-codex/"
|
||
assert meta["fetched_pages"] >= 1
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_semantics_respects_upstream_explicit_external_contract(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs):
|
||
_ = (prompt, kwargs)
|
||
return (
|
||
'{"intent":"question","question_part":"今天荔湾区天气如何?","statement_part":"",'
|
||
'"query_type":"status","retrieval_domains":["db","graph","url"],'
|
||
'"subject_focus":"unknown","scope_hint":"none","feature_focus":"none",'
|
||
'"source_policy":"external",'
|
||
'"query_profile":{"needs_private_context":false,"needs_freshness":true,"referenced_objects":["weather"],"confidence":0.96}}'
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGCoreIntentMixin, AdaptiveRAGAnswerMainlineMixin, AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
async def _fake_aplan_message(cls, *, message_text: str, source_system: str, metadata=None):
|
||
_ = (cls, message_text, source_system, metadata)
|
||
return WorkflowSemanticPlanner.deserialize_bundle(
|
||
{
|
||
"bundle_id": "semantic-weather-external",
|
||
"semantic_version": "v1",
|
||
"source_system": "adaptive_rag",
|
||
"primary_workflow": "query_flow",
|
||
"workflows": [
|
||
{
|
||
"workflow_type": "query_flow",
|
||
"workflow_action": "answer",
|
||
"target_scope": "global",
|
||
"operation_codes": ["query.answer"],
|
||
"requires_confirmation": False,
|
||
"confidence": 0.92,
|
||
"clarification_needed": False,
|
||
"metadata": {},
|
||
}
|
||
],
|
||
"message_gate": "auto",
|
||
"reply_contract": {"mode": "text", "template_key": "workflow_bundle", "summary": "query_flow"},
|
||
"diagnostics": {"message_intent": "question"},
|
||
}
|
||
)
|
||
|
||
monkeypatch.setattr(WorkflowSemanticPlanner, "aplan_message", classmethod(_fake_aplan_message))
|
||
|
||
service = _FakeService()
|
||
semantics = await service._resolve_query_semantics(
|
||
message="今天荔湾区天气如何?",
|
||
source_system="pmflow",
|
||
)
|
||
|
||
assert semantics["source_policy"] == "external"
|
||
assert semantics["effective_source_policy"] == "external"
|
||
assert semantics["retrieval_domains"] == ["url"]
|
||
assert semantics["query_profile"]["needs_freshness"] is True
|
||
assert semantics["query_profile"]["referenced_objects"] == ["weather"]
|
||
assert semantics["decision_origin"] == "query_semantics_contract"
|
||
assert semantics["source_reason"] == "query_semantics_contract"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_semantics_keeps_uncertain_contract_uncertain_without_default_external_first(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs):
|
||
_ = (prompt, kwargs)
|
||
return (
|
||
'{"intent":"question","question_part":"当前 codex 最新的模型用的是什么?","statement_part":"",'
|
||
'"query_type":"status","retrieval_domains":["db","pmflow"],'
|
||
'"subject_focus":"feature","scope_hint":"global","feature_focus":"purpose",'
|
||
'"query_profile":{"needs_private_context":false,"needs_freshness":true,"referenced_objects":["public_model"],"confidence":0.95}}'
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGCoreIntentMixin, AdaptiveRAGAnswerMainlineMixin, AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
async def _fake_aplan_message(cls, *, message_text: str, source_system: str, metadata=None):
|
||
_ = (cls, message_text, source_system, metadata)
|
||
return WorkflowSemanticPlanner.deserialize_bundle(
|
||
{
|
||
"bundle_id": "semantic-public-model-fact",
|
||
"semantic_version": "v1",
|
||
"source_system": "adaptive_rag",
|
||
"primary_workflow": "query_flow",
|
||
"workflows": [
|
||
{
|
||
"workflow_type": "query_flow",
|
||
"workflow_action": "answer",
|
||
"target_scope": "global",
|
||
"operation_codes": ["query.answer"],
|
||
"requires_confirmation": False,
|
||
"confidence": 0.9,
|
||
"clarification_needed": False,
|
||
"metadata": {},
|
||
}
|
||
],
|
||
"message_gate": "auto",
|
||
"reply_contract": {"mode": "text", "template_key": "workflow_bundle", "summary": "query_flow"},
|
||
"diagnostics": {"message_intent": "question"},
|
||
}
|
||
)
|
||
|
||
monkeypatch.setattr(WorkflowSemanticPlanner, "aplan_message", classmethod(_fake_aplan_message))
|
||
|
||
service = _FakeService()
|
||
semantics = await service._resolve_query_semantics(
|
||
message="当前 codex 最新的模型用的是什么?",
|
||
source_system="pmflow",
|
||
)
|
||
|
||
assert semantics["source_policy"] == "uncertain"
|
||
assert semantics["effective_source_policy"] == "uncertain"
|
||
assert semantics["retrieval_domains"] == ["db", "pmflow"]
|
||
assert semantics["query_profile"]["needs_freshness"] is True
|
||
assert semantics["query_profile"]["referenced_objects"] == ["public_model"]
|
||
assert semantics["decision_origin"] == "query_semantics_contract"
|
||
assert semantics["source_reason"] == "query_semantics_contract"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_semantics_promotes_url_only_freshness_contract_to_external_without_object_mapping(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs):
|
||
_ = (prompt, kwargs)
|
||
return (
|
||
'{"intent":"question","question_part":"帮我看看未来5天的广州天气如何","statement_part":"",'
|
||
'"query_type":"status","retrieval_domains":["url"],'
|
||
'"subject_focus":"signal","scope_hint":"global","feature_focus":"none",'
|
||
'"query_profile":{"needs_private_context":false,"needs_freshness":true,"referenced_objects":["weather"],"confidence":0.95}}'
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGCoreIntentMixin, AdaptiveRAGAnswerMainlineMixin, AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
async def _fake_aplan_message(cls, *, message_text: str, source_system: str, metadata=None):
|
||
_ = (cls, message_text, source_system, metadata)
|
||
return WorkflowSemanticPlanner.deserialize_bundle(
|
||
{
|
||
"bundle_id": "semantic-url-only-freshness",
|
||
"semantic_version": "v1",
|
||
"source_system": "adaptive_rag",
|
||
"primary_workflow": "query_flow",
|
||
"workflows": [
|
||
{
|
||
"workflow_type": "query_flow",
|
||
"workflow_action": "answer",
|
||
"target_scope": "global",
|
||
"operation_codes": ["query.answer"],
|
||
"requires_confirmation": False,
|
||
"confidence": 0.94,
|
||
"clarification_needed": False,
|
||
"metadata": {},
|
||
}
|
||
],
|
||
"message_gate": "auto",
|
||
"reply_contract": {"mode": "text", "template_key": "workflow_bundle", "summary": "query_flow"},
|
||
"diagnostics": {"message_intent": "weather_query"},
|
||
}
|
||
)
|
||
|
||
monkeypatch.setattr(WorkflowSemanticPlanner, "aplan_message", classmethod(_fake_aplan_message))
|
||
|
||
service = _FakeService()
|
||
semantics = await service._resolve_query_semantics(
|
||
message="帮我看看未来5天的广州天气如何",
|
||
source_system="feishu",
|
||
)
|
||
|
||
assert semantics["source_policy"] == "external"
|
||
assert semantics["effective_source_policy"] == "external"
|
||
assert semantics["retrieval_domains"] == ["url"]
|
||
assert semantics["source_reason"] == "query_semantics_url_only_external_contract"
|
||
assert semantics["decision_origin"] == "query_semantics_contract_normalized"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_semantics_preserves_internal_runtime_query_profile_for_runtime_question(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs):
|
||
_ = (prompt, kwargs)
|
||
return (
|
||
'{"intent":"question","question_part":"我们系统当前接的是哪个 codex 模型配置?","statement_part":"",'
|
||
'"query_type":"status","retrieval_domains":["url","db","pmflow"],'
|
||
'"subject_focus":"feature","scope_hint":"project","feature_focus":"catalog",'
|
||
'"source_policy":"internal",'
|
||
'"query_profile":{"needs_private_context":true,"needs_freshness":false,"referenced_objects":["system_config","model_routing"],"confidence":0.97}}'
|
||
)
|
||
|
||
class _FakeService(AdaptiveRAGCoreIntentMixin, AdaptiveRAGAnswerMainlineMixin, AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
async def _fake_aplan_message(cls, *, message_text: str, source_system: str, metadata=None):
|
||
_ = (cls, message_text, source_system, metadata)
|
||
return WorkflowSemanticPlanner.deserialize_bundle(
|
||
{
|
||
"bundle_id": "semantic-model-config-internal",
|
||
"semantic_version": "v1",
|
||
"source_system": "adaptive_rag",
|
||
"primary_workflow": "query_flow",
|
||
"workflows": [
|
||
{
|
||
"workflow_type": "query_flow",
|
||
"workflow_action": "answer",
|
||
"target_scope": "global",
|
||
"operation_codes": ["query.answer"],
|
||
"requires_confirmation": False,
|
||
"confidence": 0.9,
|
||
"clarification_needed": False,
|
||
"metadata": {},
|
||
}
|
||
],
|
||
"message_gate": "auto",
|
||
"reply_contract": {"mode": "text", "template_key": "workflow_bundle", "summary": "query_flow"},
|
||
"diagnostics": {"message_intent": "question"},
|
||
}
|
||
)
|
||
|
||
monkeypatch.setattr(WorkflowSemanticPlanner, "aplan_message", classmethod(_fake_aplan_message))
|
||
|
||
service = _FakeService()
|
||
semantics = await service._resolve_query_semantics(
|
||
message="我们系统当前接的是哪个 codex 模型配置?",
|
||
source_system="pmflow",
|
||
)
|
||
|
||
assert semantics["source_policy"] == "internal"
|
||
assert semantics["effective_source_policy"] == "internal"
|
||
assert semantics["retrieval_domains"] == ["db", "pmflow"]
|
||
assert semantics["query_profile"]["needs_private_context"] is True
|
||
assert semantics["query_profile"]["referenced_objects"] == ["system_config", "model_routing"]
|
||
assert semantics["decision_origin"] == "query_semantics_contract"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_internal_model_config_fast_answer_reads_runtime_engine_bundle() -> None:
|
||
class _FakeLLM:
|
||
async def _get_or_init_bundle(self, db, *, tenant_id: int):
|
||
_ = (db, tenant_id)
|
||
return object(), {
|
||
"active_engine_id": "low-1",
|
||
"engines": [
|
||
{
|
||
"id": "low-1",
|
||
"name": "Qwen 低阶引擎",
|
||
"provider": "glm",
|
||
"model": "qwen-plus",
|
||
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||
},
|
||
{
|
||
"id": "high-1",
|
||
"name": "OpenAI 高阶引擎",
|
||
"provider": "openai",
|
||
"model": "GLM-5",
|
||
"base_url": "https://api.edgefn.net/v1",
|
||
},
|
||
],
|
||
"settings": {
|
||
"enable_local_kb_qa": False,
|
||
},
|
||
}
|
||
|
||
def _resolve_active_engine_from_bundle(self, bundle: dict[str, object]):
|
||
engines = list(bundle.get("engines") or [])
|
||
return dict(engines[0])
|
||
|
||
async def _resolve_engine_for_call(self, db, *, tenant_id: int, function_name: str, routing_meta=None):
|
||
_ = (db, tenant_id, routing_meta)
|
||
if function_name == "adaptive_rag_answer_deep":
|
||
return {
|
||
"name": "Qwen 低阶引擎",
|
||
"provider": "glm",
|
||
"model": "qwen-plus",
|
||
"_routing_meta": {"planned_tier": "low", "executed_tier": "low"},
|
||
}
|
||
return {
|
||
"name": "Qwen 低阶引擎",
|
||
"provider": "glm",
|
||
"model": "qwen-plus",
|
||
"_routing_meta": {"planned_tier": "low", "executed_tier": "low"},
|
||
}
|
||
|
||
class _FakeService(AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
service = _FakeService()
|
||
fast_result = await service._build_internal_model_config_fast_answer(
|
||
question="我们系统当前接的是哪个 codex 模型配置?",
|
||
query_semantics={
|
||
"effective_source_policy": "internal",
|
||
"query_type": "status",
|
||
"operation_contract": {
|
||
"query_type": "status",
|
||
},
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["system_config", "model_routing"],
|
||
},
|
||
"target_resolution": {
|
||
"target_entity_type": "config",
|
||
"target_ref": "config:model_routing",
|
||
},
|
||
},
|
||
)
|
||
|
||
assert isinstance(fast_result, dict)
|
||
assert "qwen-plus" in str(fast_result["answer"])
|
||
assert "adaptive_rag_answer_normal" in str(fast_result["answer"])
|
||
assert "enable_local_kb_qa=false" in str(fast_result["answer"])
|
||
assert fast_result["query_type"] == "status"
|
||
assert fast_result["prompt_variant_id"] == "internal_model_config_direct"
|
||
labels = [str(item.get("label") or "") for item in list(fast_result.get("sources") or [])]
|
||
assert "Integration[llm_engine]" in labels
|
||
assert "Integration[model_routing:adaptive_rag_answer_normal]" in labels
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_internal_model_config_fast_answer_skips_non_config_target_resolution() -> None:
|
||
class _FakeLLM:
|
||
async def _get_or_init_bundle(self, db, *, tenant_id: int): # pragma: no cover - should not be called
|
||
raise AssertionError(f"bundle lookup should be skipped: {db}, {tenant_id}")
|
||
|
||
def _resolve_active_engine_from_bundle(self, bundle: dict[str, object]): # pragma: no cover - should not be called
|
||
raise AssertionError(f"active engine should be skipped: {bundle}")
|
||
|
||
async def _resolve_engine_for_call(self, db, *, tenant_id: int, function_name: str, routing_meta=None): # pragma: no cover - should not be called
|
||
raise AssertionError(f"engine lookup should be skipped: {function_name}, {routing_meta}")
|
||
|
||
class _FakeService(AdaptiveRAGDeepGuardAnswerMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
service = _FakeService()
|
||
fast_result = await service._build_internal_model_config_fast_answer(
|
||
question="系统里有没有知识库?",
|
||
query_semantics={
|
||
"effective_source_policy": "internal",
|
||
"query_type": "existence",
|
||
"operation_contract": {
|
||
"query_type": "existence",
|
||
},
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["system_config", "model_routing"],
|
||
},
|
||
"target_resolution": {
|
||
"target_entity_type": "project",
|
||
"target_ref": "project:72",
|
||
},
|
||
},
|
||
)
|
||
|
||
assert fast_result is None
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_second_pass_query_skips_internal_hints_for_effective_external_source_policy() -> None:
|
||
class _FakeService(AdaptiveRAGMultiStepCoreMixin):
|
||
async def entity_resolution(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"entity_resolution should be skipped: {kwargs}")
|
||
|
||
async def get_project_manifest(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"get_project_manifest should be skipped: {kwargs}")
|
||
|
||
service = _FakeService()
|
||
revised, meta = await service._build_second_pass_query(
|
||
question="今天荔湾区天气如何?",
|
||
project_id=None,
|
||
query_semantics={"effective_source_policy": "external"},
|
||
)
|
||
|
||
assert revised == "今天荔湾区天气如何?"
|
||
assert meta["mode"] == "external_source_policy_skip_internal_hint"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_multi_step_search_skips_internal_enrichment_for_effective_external_source_policy() -> None:
|
||
class _FakeService(AdaptiveRAGMultiStepFlowMixin, AdaptiveRAGMultiStepCoreMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.context_cache = SimpleNamespace(
|
||
get_system_snapshot=self._unexpected_system_snapshot,
|
||
)
|
||
|
||
async def _unexpected_system_snapshot(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"system snapshot should be skipped: {kwargs}")
|
||
|
||
async def entity_resolution(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"entity_resolution should be skipped: {kwargs}")
|
||
|
||
async def get_project_manifest(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"get_project_manifest should be skipped: {kwargs}")
|
||
|
||
def _match_feature_catalog_entries(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"feature catalog should be skipped: {kwargs}")
|
||
|
||
def _retrieve_feature_catalog_context(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"feature retrieval should be skipped: {kwargs}")
|
||
|
||
async def _run_single_retrieval_pass(self, **kwargs):
|
||
_ = kwargs
|
||
return (
|
||
[
|
||
RetrievedItem(
|
||
source="url",
|
||
content="广州荔湾区今日天气:多云转阵雨。",
|
||
score=0.93,
|
||
source_label="URL[weather.example]",
|
||
metadata={"url": "https://weather.example/liwan"},
|
||
)
|
||
],
|
||
{
|
||
"db_query_ms": 0,
|
||
"vector_search_ms": 12,
|
||
"graph_query_ms": 0,
|
||
"db_timeout": False,
|
||
"vector_timeout": False,
|
||
"graph_timeout": False,
|
||
},
|
||
)
|
||
|
||
service = _FakeService()
|
||
rows, meta = await service._multi_step_search(
|
||
question="今天荔湾区天气如何?",
|
||
domains=["url"],
|
||
project_id=None,
|
||
source_chat_id=None,
|
||
query_semantics={"effective_source_policy": "external"},
|
||
session_state=None,
|
||
)
|
||
|
||
assert len(rows) == 1
|
||
assert rows[0].source == "url"
|
||
assert meta["domains_used"] == ["url"]
|
||
assert all(item.get("tool") != "freshness_guard" for item in meta["tool_trace"])
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_answer_scene_semantics_uses_source_policy_without_llm_call() -> None:
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"scene semantics should not hit llm: {prompt} | {kwargs}")
|
||
|
||
class _FakeService(AdaptiveRAGDeepGuardAnswerMixin, AdaptiveRAGAnswerMainlineMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
service = _FakeService()
|
||
semantics = await service._resolve_answer_scene_semantics(
|
||
question="今天荔湾区天气如何?",
|
||
effective_question="今天荔湾区天气如何?",
|
||
asked_via="pmflow",
|
||
message_intent="casual_chat",
|
||
query_type="status",
|
||
conversation_text="无",
|
||
source_policy="external",
|
||
)
|
||
|
||
assert semantics["scene_key"] == "industry_trend"
|
||
assert semantics["confidence"] == 1.0
|
||
assert semantics["reason"] == "source_policy:external"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_answer_scene_semantics_uses_query_scope_for_external_project_scene() -> None:
|
||
class _FakeLLM:
|
||
async def generate(self, prompt: str, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"scene semantics should not hit llm: {prompt} | {kwargs}")
|
||
|
||
class _FakeService(AdaptiveRAGDeepGuardAnswerMixin, AdaptiveRAGAnswerMainlineMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.llm = _FakeLLM()
|
||
|
||
service = _FakeService()
|
||
semantics = await service._resolve_answer_scene_semantics(
|
||
question="现在这个情况会有什么影响?",
|
||
effective_question="现在这个情况会有什么影响?",
|
||
asked_via="pmflow",
|
||
message_intent="question",
|
||
query_type="explain",
|
||
conversation_text="无",
|
||
source_policy="external",
|
||
project_id=12,
|
||
query_semantics={"scope_hint": "project"},
|
||
)
|
||
|
||
assert semantics["scene_key"] == "external_trend"
|
||
assert semantics["reason"] == "source_policy:external"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_route_gateway_uses_upstream_internal_contract_without_runtime_probes() -> None:
|
||
class _FakeService(AdaptiveRAGQueryRouteGatewayMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
async def _run_internal_route_probe(self, **kwargs):
|
||
raise AssertionError(f"runtime internal probe should not be called: {kwargs}")
|
||
|
||
async def _run_external_route_probe(self, **kwargs):
|
||
raise AssertionError(f"runtime external probe should not be called: {kwargs}")
|
||
|
||
async def _retrieve_query_route_memory_prior(self, **kwargs):
|
||
raise AssertionError(f"route memory prior should not be called: {kwargs}")
|
||
|
||
service = _FakeService()
|
||
decision = await service._run_query_route_gateway(
|
||
question="我们系统当前接的是哪个 codex 模型配置?",
|
||
asked_via="pmflow_manual",
|
||
project_id=None,
|
||
source_chat_id=None,
|
||
query_semantics={
|
||
"source_policy": "internal",
|
||
"effective_source_policy": "internal",
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["system_config", "model_routing"],
|
||
}
|
||
},
|
||
source_policy_hint=None,
|
||
)
|
||
|
||
assert decision["route_mode"] == "internal"
|
||
assert decision["effective_source_policy"] == "internal"
|
||
assert decision["decision_origin"] == "query_semantics_contract"
|
||
assert decision["reply_contract"]["head"] == "内部知识库查询:"
|
||
assert decision["probe_snapshot"]["contract"]["source_policy"] == "internal"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_route_gateway_keeps_uncertain_contract_as_clarify_without_external_first_fallback() -> None:
|
||
class _FakeService(AdaptiveRAGQueryRouteGatewayMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
async def _run_internal_route_probe(self, **kwargs):
|
||
raise AssertionError(f"runtime internal probe should not be called: {kwargs}")
|
||
|
||
async def _run_external_route_probe(self, **kwargs):
|
||
raise AssertionError(f"runtime external probe should not be called: {kwargs}")
|
||
|
||
async def _retrieve_query_route_memory_prior(self, **kwargs):
|
||
raise AssertionError(f"route memory prior should not be called: {kwargs}")
|
||
|
||
service = _FakeService()
|
||
decision = await service._run_query_route_gateway(
|
||
question="当前 codex 最新的模型用的是什么?",
|
||
asked_via="pmflow_manual",
|
||
project_id=None,
|
||
source_chat_id=None,
|
||
query_semantics={
|
||
"source_policy": "uncertain",
|
||
"effective_source_policy": "uncertain",
|
||
"query_profile": {
|
||
"needs_private_context": False,
|
||
"needs_freshness": True,
|
||
"referenced_objects": ["public_model"],
|
||
}
|
||
},
|
||
source_policy_hint=None,
|
||
)
|
||
|
||
assert decision["route_mode"] == "uncertain"
|
||
assert decision["effective_source_policy"] == "uncertain"
|
||
assert decision["decision_origin"] == "query_semantics_contract"
|
||
assert decision["reply_contract"]["mode"] == "clarify"
|
||
assert "系统内部配置" in str(decision["clarification_text"])
|
||
assert decision["probe_snapshot"]["contract"]["source_policy"] == "uncertain"
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_route_gateway_builds_external_execution_recipe_for_current_project() -> None:
|
||
class _FakeService(AdaptiveRAGQueryRouteGatewayMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
service = _FakeService()
|
||
decision = await service._run_query_route_gateway(
|
||
question="这个方案适不适合我们当前项目?",
|
||
asked_via="pmflow_manual",
|
||
project_id=17,
|
||
source_chat_id="pmflow-project-17",
|
||
query_semantics={
|
||
"message_intent": "question",
|
||
"query_type": "explain",
|
||
"source_policy": "external",
|
||
"effective_source_policy": "external",
|
||
"query_profile": {
|
||
"needs_private_context": False,
|
||
"needs_freshness": True,
|
||
"referenced_objects": ["public_solution"],
|
||
},
|
||
},
|
||
source_policy_hint=None,
|
||
)
|
||
|
||
packet = decision["project_operating_context"]
|
||
recipe = decision["execution_recipe"]
|
||
|
||
assert packet["current_project_id"] == 17
|
||
assert packet["page_anchor"]["project_id"] == 17
|
||
assert packet["page_anchor"]["source_chat_id"] == "pmflow-project-17"
|
||
assert recipe == {
|
||
"primary_lane": "external",
|
||
"fit_brief_required": True,
|
||
"external_evidence_required": True,
|
||
"plan_handoff_mode": "offer_after_answer",
|
||
}
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_route_gateway_keeps_explicit_requirement_plan_recipe() -> None:
|
||
class _FakeService(AdaptiveRAGQueryRouteGatewayMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
service = _FakeService()
|
||
decision = await service._run_query_route_gateway(
|
||
question="那按这个结论继续收需求",
|
||
asked_via="pmflow_manual",
|
||
project_id=17,
|
||
source_chat_id="pmflow-project-17",
|
||
query_semantics={
|
||
"message_intent": "requirement_plan",
|
||
"query_type": "plan",
|
||
"source_policy": "internal",
|
||
"effective_source_policy": "internal",
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["project_requirement"],
|
||
},
|
||
},
|
||
source_policy_hint=None,
|
||
)
|
||
|
||
assert decision["execution_recipe"] == {
|
||
"primary_lane": "plan",
|
||
"fit_brief_required": False,
|
||
"external_evidence_required": False,
|
||
"plan_handoff_mode": "explicit_plan_now",
|
||
}
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_route_gateway_can_disable_unified_execution_recipe(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
class _FakeService(AdaptiveRAGQueryRouteGatewayMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
|
||
monkeypatch.setenv("PMFLOW_ENABLE_UNIFIED_EXECUTION_RECIPE", "0")
|
||
service = _FakeService()
|
||
decision = await service._run_query_route_gateway(
|
||
question="这个方案适不适合当前项目?",
|
||
asked_via="pmflow_manual",
|
||
project_id=17,
|
||
source_chat_id="pmflow-project-17",
|
||
query_semantics={
|
||
"message_intent": "question",
|
||
"query_type": "explain",
|
||
"source_policy": "external",
|
||
"effective_source_policy": "external",
|
||
"query_profile": {
|
||
"needs_private_context": False,
|
||
"needs_freshness": True,
|
||
"referenced_objects": ["public_solution"],
|
||
},
|
||
},
|
||
source_policy_hint=None,
|
||
)
|
||
|
||
assert decision["execution_recipe"] == {
|
||
"primary_lane": "external",
|
||
"fit_brief_required": False,
|
||
"external_evidence_required": True,
|
||
"plan_handoff_mode": "none",
|
||
}
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_query_route_clarification_response_stores_pending_route_protocol() -> None:
|
||
class _FakeDB:
|
||
def add(self, obj) -> None:
|
||
obj.id = 1
|
||
|
||
async def flush(self) -> None:
|
||
return None
|
||
|
||
async def commit(self) -> None:
|
||
return None
|
||
|
||
async def refresh(self, obj) -> None:
|
||
_ = obj
|
||
return None
|
||
|
||
class _FakeContextCache:
|
||
async def append_session_messages(self, **kwargs) -> None:
|
||
_ = kwargs
|
||
return None
|
||
|
||
class _FakeService(
|
||
AdaptiveRAGAnswerPrecheckMixin,
|
||
AdaptiveRAGQueryRouteGatewayMixin,
|
||
AdaptiveRAGFollowupAnchorMixin,
|
||
):
|
||
def __init__(self) -> None:
|
||
self.db = _FakeDB()
|
||
self.tenant_id = 1
|
||
self.context_cache = _FakeContextCache()
|
||
|
||
def _to_naive_utc(self, value):
|
||
return value
|
||
|
||
def _build_qa_record_runtime_fields(self, **kwargs):
|
||
_ = kwargs
|
||
return {
|
||
"policy_version": "test",
|
||
"citations": [],
|
||
"sources_used": [],
|
||
"evidence_pack": {},
|
||
"tool_trace": [],
|
||
}
|
||
|
||
def _build_qa_record_external_fields(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
def _build_response_payload(self, **kwargs):
|
||
return kwargs
|
||
|
||
async def _update_session_state_after_turn(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
service = _FakeService()
|
||
state = SimpleNamespace(
|
||
last_subject=None,
|
||
last_entity_ids=[],
|
||
last_intent=None,
|
||
ambiguity="unknown",
|
||
rewrite_history=[],
|
||
pending_clarification=None,
|
||
pending_clarification_expires_at=None,
|
||
)
|
||
|
||
result = await service._maybe_return_clarification_response(
|
||
clarification_payload={
|
||
"answer": "你是想问外部公开事实,还是系统内部配置/项目对象?",
|
||
"code": "query_route_clarify",
|
||
"candidates": [],
|
||
"missing_fields": ["query_route"],
|
||
},
|
||
started=0.0,
|
||
clean_q="当前系统内是否有为采购经理提出的需求?",
|
||
effective_q="当前系统内是否有为采购经理提出的需求?",
|
||
query_type="existence",
|
||
asked_by=1,
|
||
asked_via="feishu",
|
||
project_id=None,
|
||
session_id=11,
|
||
turn_index=2,
|
||
state_row=state,
|
||
rewrite_meta={"mode": "direct"},
|
||
context_session_id=11,
|
||
active_thinking_mode="normal",
|
||
route_decision={
|
||
"route_mode": "uncertain",
|
||
"effective_source_policy": "uncertain",
|
||
"reply_contract": {
|
||
"mode": "clarify",
|
||
"clarification_text": "你是想问外部公开事实,还是系统内部配置/项目对象?",
|
||
},
|
||
},
|
||
)
|
||
|
||
assert result["clarification_code"] == "query_route_clarify"
|
||
assert state.pending_clarification["kind"] == "query_route_selection"
|
||
assert state.pending_clarification["original_question"] == "当前系统内是否有为采购经理提出的需求?"
|
||
assert state.pending_clarification["option_map"] == {"A": "external", "B": "internal"}
|
||
assert state.pending_clarification["options"] == [
|
||
{"key": "A", "label": "外部公开事实"},
|
||
{"key": "B", "label": "系统内部配置/项目对象"},
|
||
]
|
||
|
||
|
||
def test_query_route_pending_clarification_consumes_protocol_option_label() -> None:
|
||
class _FakeService(AdaptiveRAGFollowupAnchorMixin):
|
||
def _to_naive_utc(self, value):
|
||
return value
|
||
|
||
service = _FakeService()
|
||
state = SimpleNamespace(
|
||
pending_clarification={
|
||
"kind": "query_route_selection",
|
||
"original_question": "当前系统内是否有为采购经理提出的需求?",
|
||
"attempts": 1,
|
||
"option_map": {"A": "external", "B": "internal"},
|
||
"options": [
|
||
{"key": "A", "label": "外部公开事实"},
|
||
{"key": "B", "label": "系统内部配置/项目对象"},
|
||
],
|
||
},
|
||
pending_clarification_expires_at=None,
|
||
)
|
||
|
||
consumed = service._consume_pending_clarification_selection(
|
||
question="内部配置",
|
||
session_state=state,
|
||
)
|
||
|
||
assert consumed == {
|
||
"effective_question": "当前系统内是否有为采购经理提出的需求?",
|
||
"source_policy_hint": "internal",
|
||
}
|
||
assert state.pending_clarification is None
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_prepare_answer_runtime_passes_consumed_query_route_hint_to_preview() -> None:
|
||
class _FakeService(AdaptiveRAGAnswerMainlineMixin, AdaptiveRAGFollowupAnchorMixin):
|
||
def __init__(self) -> None:
|
||
self.preview_kwargs: dict[str, object] = {}
|
||
self.state_row = SimpleNamespace(
|
||
last_subject=None,
|
||
last_entity_ids=[],
|
||
last_intent=None,
|
||
ambiguity="unknown",
|
||
rewrite_history=[],
|
||
pending_clarification={
|
||
"kind": "query_route_selection",
|
||
"original_question": "当前系统内是否有为采购经理提出的需求?",
|
||
"attempts": 1,
|
||
"option_map": {"A": "external", "B": "internal"},
|
||
"options": [
|
||
{"key": "A", "label": "外部公开事实"},
|
||
{"key": "B", "label": "系统内部配置/项目对象"},
|
||
],
|
||
},
|
||
pending_clarification_expires_at=None,
|
||
)
|
||
|
||
def _to_naive_utc(self, value):
|
||
return value
|
||
|
||
def _build_external_conversation_key(self, **kwargs):
|
||
_ = kwargs
|
||
return "scope:feishu:test"
|
||
|
||
def _set_active_external_qa_context(self, **kwargs):
|
||
_ = kwargs
|
||
|
||
async def _touch_session_for_new_record(self, **kwargs):
|
||
_ = kwargs
|
||
return 11, 2
|
||
|
||
def _external_session_id_from_key(self, key: str):
|
||
_ = key
|
||
return 11
|
||
|
||
async def _load_recent_conversation(self, **kwargs):
|
||
_ = kwargs
|
||
return []
|
||
|
||
async def _ensure_session_state(self, **kwargs):
|
||
_ = kwargs
|
||
return self.state_row
|
||
|
||
async def _resolve_followup_anchor(self, **kwargs):
|
||
_ = kwargs
|
||
return FollowupAnchorResolution()
|
||
|
||
def _rewrite_question_with_state(self, **kwargs):
|
||
return kwargs["question"], {"mode": "direct"}
|
||
|
||
async def preview_query_route_decision(self, **kwargs):
|
||
self.preview_kwargs = dict(kwargs)
|
||
return {
|
||
"query_semantics": {
|
||
"source_policy": "internal",
|
||
"effective_source_policy": "internal",
|
||
"query_type": "existence",
|
||
"retrieval_domains": ["db", "pmflow"],
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["system_config"],
|
||
},
|
||
},
|
||
"route_decision": {
|
||
"route_mode": "internal",
|
||
"effective_source_policy": "internal",
|
||
"decision_origin": "upstream_hint",
|
||
"reply_contract": {
|
||
"mode": "explicit_source",
|
||
"head": "内部知识库查询:",
|
||
"tail_hint": None,
|
||
},
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["system_config"],
|
||
},
|
||
"probe_snapshot_summary": {},
|
||
"probe_snapshot": {},
|
||
},
|
||
}
|
||
|
||
async def _resolve_answer_scene_semantics(self, **kwargs):
|
||
_ = kwargs
|
||
return {"scene_key": "ai_pmo", "deep_intent": "none"}
|
||
|
||
def _normalize_action_mode(self, value: str) -> str:
|
||
return str(value or "").strip().lower() or "auto"
|
||
|
||
async def _resolve_action_mode_async(self, **kwargs):
|
||
_ = kwargs
|
||
return ACTION_MODE_READ
|
||
|
||
service = _FakeService()
|
||
runtime = await service._prepare_answer_runtime(
|
||
clean_q="内部配置",
|
||
asked_by=1,
|
||
asked_via="feishu",
|
||
project_id=None,
|
||
source_chat_id="oc_test_chat",
|
||
session_id=11,
|
||
conversation_key=None,
|
||
force_new_session=False,
|
||
action_mode="auto",
|
||
)
|
||
|
||
assert runtime["resolved_question"] == "当前系统内是否有为采购经理提出的需求?"
|
||
assert service.preview_kwargs["question"] == "当前系统内是否有为采购经理提出的需求?"
|
||
assert service.preview_kwargs["source_policy_hint"] == "internal"
|
||
assert service.state_row.pending_clarification is None
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_prepare_answer_runtime_recomputes_route_after_consuming_query_route_selection() -> None:
|
||
class _FakeService(
|
||
AdaptiveRAGAnswerMainlineMixin,
|
||
AdaptiveRAGFollowupAnchorMixin,
|
||
AdaptiveRAGQueryRouteGatewayMixin,
|
||
):
|
||
def __init__(self) -> None:
|
||
self.preview_kwargs: dict[str, object] = {}
|
||
self.state_row = SimpleNamespace(
|
||
last_subject=None,
|
||
last_entity_ids=[],
|
||
last_intent=None,
|
||
ambiguity="unknown",
|
||
rewrite_history=[],
|
||
pending_clarification={
|
||
"kind": "query_route_selection",
|
||
"original_question": "当前系统内是否有为采购经理提出的需求?",
|
||
"attempts": 1,
|
||
"option_map": {"A": "external", "B": "internal"},
|
||
"options": [
|
||
{"key": "A", "label": "外部公开事实"},
|
||
{"key": "B", "label": "系统内部配置/项目对象"},
|
||
],
|
||
},
|
||
pending_clarification_expires_at=None,
|
||
)
|
||
|
||
def _to_naive_utc(self, value):
|
||
return value
|
||
|
||
def _build_external_conversation_key(self, **kwargs):
|
||
_ = kwargs
|
||
return "scope:feishu:test"
|
||
|
||
def _set_active_external_qa_context(self, **kwargs):
|
||
_ = kwargs
|
||
|
||
async def _touch_session_for_new_record(self, **kwargs):
|
||
_ = kwargs
|
||
return 11, 2
|
||
|
||
def _external_session_id_from_key(self, key: str):
|
||
_ = key
|
||
return 11
|
||
|
||
async def _load_recent_conversation(self, **kwargs):
|
||
_ = kwargs
|
||
return []
|
||
|
||
async def _ensure_session_state(self, **kwargs):
|
||
_ = kwargs
|
||
return self.state_row
|
||
|
||
async def _resolve_followup_anchor(self, **kwargs):
|
||
_ = kwargs
|
||
return FollowupAnchorResolution()
|
||
|
||
def _rewrite_question_with_state(self, **kwargs):
|
||
return kwargs["question"], {"mode": "direct"}
|
||
|
||
async def preview_query_route_decision(self, **kwargs):
|
||
self.preview_kwargs = dict(kwargs)
|
||
return {
|
||
"query_semantics": {
|
||
"source_policy": "internal",
|
||
"effective_source_policy": "internal",
|
||
"query_type": "existence",
|
||
"retrieval_domains": ["db", "pmflow"],
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["project_object"],
|
||
},
|
||
},
|
||
"route_decision": {
|
||
"route_mode": "internal",
|
||
"effective_source_policy": "internal",
|
||
"decision_origin": "upstream_hint",
|
||
"reply_contract": {
|
||
"mode": "explicit_source",
|
||
"head": "内部知识库查询:",
|
||
"tail_hint": None,
|
||
},
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["project_object"],
|
||
},
|
||
"probe_snapshot_summary": {},
|
||
"probe_snapshot": {},
|
||
},
|
||
}
|
||
|
||
async def _resolve_answer_scene_semantics(self, **kwargs):
|
||
_ = kwargs
|
||
return {"scene_key": "ai_pmo", "deep_intent": "none"}
|
||
|
||
def _normalize_action_mode(self, value: str) -> str:
|
||
return str(value or "").strip().lower() or "auto"
|
||
|
||
async def _resolve_action_mode_async(self, **kwargs):
|
||
_ = kwargs
|
||
return ACTION_MODE_READ
|
||
|
||
service = _FakeService()
|
||
runtime = await service._prepare_answer_runtime(
|
||
clean_q="内部配置",
|
||
asked_by=1,
|
||
asked_via="feishu",
|
||
project_id=None,
|
||
source_chat_id="oc_test_chat",
|
||
session_id=11,
|
||
conversation_key=None,
|
||
force_new_session=False,
|
||
action_mode="auto",
|
||
precomputed_query_semantics={
|
||
"source_policy": "uncertain",
|
||
"effective_source_policy": "uncertain",
|
||
"query_type": "explain",
|
||
"retrieval_domains": ["db", "pmflow", "doc"],
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["system_config"],
|
||
"confidence": 0.9,
|
||
},
|
||
},
|
||
route_decision={
|
||
"route_mode": "uncertain",
|
||
"effective_source_policy": "uncertain",
|
||
"decision_origin": "query_semantics_contract",
|
||
"reply_contract": {
|
||
"mode": "clarify",
|
||
"clarification_text": "你是想问外部公开事实,还是系统内部配置/项目对象?",
|
||
},
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["system_config"],
|
||
"confidence": 0.9,
|
||
},
|
||
"probe_snapshot_summary": {},
|
||
"probe_snapshot": {},
|
||
},
|
||
)
|
||
|
||
assert runtime["resolved_question"] == "当前系统内是否有为采购经理提出的需求?"
|
||
assert service.preview_kwargs["question"] == "当前系统内是否有为采购经理提出的需求?"
|
||
assert service.preview_kwargs["source_policy_hint"] == "internal"
|
||
assert service.preview_kwargs["query_semantics"] == {}
|
||
assert runtime["query_type"] == "existence"
|
||
assert runtime["route_decision"]["route_mode"] == "internal"
|
||
assert service.state_row.pending_clarification is None
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_render_answer_with_route_contract_adds_explicit_source_prefix_and_summary() -> None:
|
||
class _FakeService(AdaptiveRAGQueryRouteGatewayMixin):
|
||
pass
|
||
|
||
service = _FakeService()
|
||
formatted = service._render_answer_with_route_contract(
|
||
answer="当前活动引擎是 Qwen 低阶引擎。",
|
||
sources=[
|
||
{"source": "db", "label": "Integration[llm_engine]", "score": 0.99},
|
||
{"source": "db", "label": "Integration[model_routing:adaptive_rag_answer_normal]", "score": 0.98},
|
||
],
|
||
route_decision={
|
||
"route_mode": "internal",
|
||
"reply_contract": {
|
||
"head": "内部知识库查询:",
|
||
"tail_hint": None,
|
||
},
|
||
},
|
||
)
|
||
|
||
assert formatted.startswith("内部知识库查询:")
|
||
assert "来源摘要:" in formatted
|
||
assert "Integration[llm_engine]" in formatted
|
||
|
||
|
||
def test_build_response_payload_exposes_execution_recipe_and_plan_handoff() -> None:
|
||
class _FakeService(AdaptiveRAGAnswerPayloadMixin):
|
||
def _to_float(self, value, default=0.0):
|
||
try:
|
||
return float(value)
|
||
except Exception:
|
||
return float(default)
|
||
|
||
def _normalize_thinking_mode(self, thinking_mode: str) -> str:
|
||
return "deep" if str(thinking_mode or "").strip().lower() == "deep" else "normal"
|
||
|
||
def _normalize_query_route_decision(self, payload):
|
||
return dict(payload or {})
|
||
|
||
def _build_policy_version(self, *, prompt_variant_id: str | None = None) -> str:
|
||
return f"retrieval-v2.1|routing:unknown|prompt:{prompt_variant_id or 'unknown'}"
|
||
|
||
service = _FakeService()
|
||
payload = service._build_response_payload(
|
||
answer="建议先做一轮小范围验证。",
|
||
sources=[
|
||
{
|
||
"source": "url",
|
||
"label": "OpenAI Docs",
|
||
"score": 0.93,
|
||
"metadata": {"target_url": "https://platform.openai.com/docs"},
|
||
}
|
||
],
|
||
domains_used=["url"],
|
||
response_time_ms=32,
|
||
db_query_ms=0,
|
||
vector_search_ms=0,
|
||
query_type="explain",
|
||
confidence=0.93,
|
||
route_decision={"route_mode": "external", "effective_source_policy": "external"},
|
||
project_operating_context={
|
||
"current_project_id": 17,
|
||
"current_repo_scope": "pmflow-main",
|
||
"page_anchor": {"project_id": 17},
|
||
"thread_anchor": {},
|
||
"fit_scope": "current_project_and_repo",
|
||
"decision_strategy": "answer_then_offer_plan",
|
||
},
|
||
execution_recipe={
|
||
"primary_lane": "external",
|
||
"fit_brief_required": True,
|
||
"external_evidence_required": True,
|
||
"plan_handoff_mode": "offer_after_answer",
|
||
},
|
||
)
|
||
|
||
assert payload["project_operating_context"]["current_project_id"] == 17
|
||
assert payload["execution_recipe"]["primary_lane"] == "external"
|
||
assert payload["handoff_mode"] == "offer_after_answer"
|
||
assert payload["can_enter_plan"] is True
|
||
assert "进入 plan" in payload["recommended_next_step"]
|
||
|
||
|
||
def test_response_payload_can_disable_default_offer_plan_rendering(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
class _FakeService(AdaptiveRAGAnswerPayloadMixin):
|
||
def _to_float(self, value, default: float = 0.0) -> float:
|
||
try:
|
||
return float(value)
|
||
except Exception:
|
||
return float(default)
|
||
|
||
def _normalize_thinking_mode(self, thinking_mode: str) -> str:
|
||
return "deep" if str(thinking_mode or "").strip().lower() == "deep" else "normal"
|
||
|
||
def _normalize_query_route_decision(self, payload):
|
||
return dict(payload or {})
|
||
|
||
def _build_policy_version(self, *, prompt_variant_id: str | None = None) -> str:
|
||
return f"retrieval-v2.1|routing:unknown|prompt:{prompt_variant_id or 'unknown'}"
|
||
|
||
monkeypatch.setenv("PMFLOW_ENABLE_DEFAULT_PLAN_HANDOFF_RENDERING", "0")
|
||
service = _FakeService()
|
||
payload = service._build_response_payload(
|
||
answer="建议先做一轮小范围验证。",
|
||
sources=[
|
||
{
|
||
"source": "url",
|
||
"label": "OpenAI Docs",
|
||
"score": 0.93,
|
||
"metadata": {"target_url": "https://platform.openai.com/docs"},
|
||
}
|
||
],
|
||
domains_used=["url"],
|
||
response_time_ms=32,
|
||
db_query_ms=0,
|
||
vector_search_ms=0,
|
||
query_type="explain",
|
||
confidence=0.93,
|
||
route_decision={"route_mode": "external", "effective_source_policy": "external"},
|
||
project_operating_context={
|
||
"current_project_id": 17,
|
||
"current_repo_scope": "pmflow-main",
|
||
"page_anchor": {"project_id": 17},
|
||
"thread_anchor": {},
|
||
"fit_scope": "current_project_and_repo",
|
||
"decision_strategy": "answer_then_offer_plan",
|
||
},
|
||
execution_recipe={
|
||
"primary_lane": "external",
|
||
"fit_brief_required": True,
|
||
"external_evidence_required": True,
|
||
"plan_handoff_mode": "offer_after_answer",
|
||
},
|
||
)
|
||
|
||
assert payload["handoff_mode"] is None
|
||
assert payload["can_enter_plan"] is False
|
||
assert payload["recommended_next_step"] is None
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_meta_followup_without_real_anchor_does_not_return_no_route_record_answer() -> None:
|
||
class _FakeService(AdaptiveRAGFollowupAnchorMixin):
|
||
async def _resolve_anchor_qa_record(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
service = _FakeService()
|
||
payload = await service._build_meta_followup_payload(
|
||
question="我们系统当前接的是哪个 codex 模型配置?",
|
||
anchor_resolution=FollowupAnchorResolution(
|
||
followup_mode="meta",
|
||
anchor_mode="no_anchor",
|
||
is_meta_followup=True,
|
||
meta_followup_kind="reply_model_identity",
|
||
),
|
||
conversation_scope_key="scope:test",
|
||
)
|
||
|
||
assert payload is None
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_execute_mainline_skips_followup_clarification_after_state_subject_rewrite() -> None:
|
||
class _FakeContextCache:
|
||
async def get_system_snapshot(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
async def get_user_snapshot(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
class _FakeService(
|
||
AdaptiveRAGAnswerMainlineMixin,
|
||
AdaptiveRAGFollowupAnchorMixin,
|
||
):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.context_cache = _FakeContextCache()
|
||
self.clarification_payload = None
|
||
self.retrieval_kwargs: dict[str, object] = {}
|
||
|
||
def _plan_query_runtime(self, **kwargs):
|
||
_ = kwargs
|
||
return SimpleNamespace(
|
||
thinking_mode="normal",
|
||
retrieval_answer_mode="retrieval_normal",
|
||
budget=SimpleNamespace(max_steps=2, vector_top_k=3),
|
||
allow_fast_path=False,
|
||
reason="consultant_mode",
|
||
)
|
||
|
||
def _build_route_clarification_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _build_meta_followup_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _maybe_return_clarification_response(self, **kwargs):
|
||
self.clarification_payload = kwargs.get("clarification_payload")
|
||
if self.clarification_payload is not None:
|
||
return {
|
||
"processing_stage": "clarification",
|
||
"answer": self.clarification_payload.get("answer"),
|
||
}
|
||
return None
|
||
|
||
async def _maybe_return_meta_followup_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _get_active_external_qa_context(self):
|
||
return {}
|
||
|
||
def _build_operation_contract(self, *, question: str, query_semantics):
|
||
_ = (question, query_semantics)
|
||
return {
|
||
"query_type": "existence",
|
||
"answer_contract": "yes_no_with_evidence",
|
||
}
|
||
|
||
async def _resolve_internal_target(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
def _mirror_internal_context_to_query_semantics(self, *, query_semantics, **kwargs):
|
||
_ = kwargs
|
||
return dict(query_semantics)
|
||
|
||
def _build_internal_query_plan(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
def _build_internal_query_context_packet(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _maybe_return_internal_context_precheck_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _build_internal_model_config_fast_answer(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _build_snapshot_direct_answer(self, *args, **kwargs):
|
||
_ = (args, kwargs)
|
||
return None
|
||
|
||
async def _maybe_return_snapshot_direct_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _try_glm_project_semantic_answer(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _build_retrieval_response(self, **kwargs):
|
||
self.retrieval_kwargs = dict(kwargs)
|
||
return {
|
||
"processing_stage": "retrieval",
|
||
"answer": "内部知识库查询:当前系统内已存在采购经理提出的需求。",
|
||
}
|
||
|
||
service = _FakeService()
|
||
rewritten_question = "围绕“采购经理每日实时库存上报服务”回答:帮我看下采购经理那个需求是否存在系统?"
|
||
|
||
result = await service._execute_base_answer_mainline(
|
||
clean_q="帮我看下采购经理那个需求是否存在系统?",
|
||
asked_by=1,
|
||
asked_via="feishu",
|
||
project_id=None,
|
||
source_chat_id="chat-1",
|
||
session_id=11,
|
||
turn_index=2,
|
||
context_session_id=11,
|
||
conversation=[],
|
||
state_row=SimpleNamespace(
|
||
last_subject="采购经理每日实时库存上报服务",
|
||
last_entity_ids=[123],
|
||
last_intent="question",
|
||
ambiguity="clear",
|
||
rewrite_history=[],
|
||
pending_clarification=None,
|
||
pending_clarification_expires_at=None,
|
||
),
|
||
effective_q=rewritten_question,
|
||
rewrite_meta={"mode": "state_subject", "subject": "采购经理每日实时库存上报服务"},
|
||
query_type="existence",
|
||
query_semantics={
|
||
"source_policy": "internal",
|
||
"effective_source_policy": "internal",
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["project_object"],
|
||
},
|
||
},
|
||
retrieval_domains=["db", "pmflow"],
|
||
scene_semantics={"scene_key": "ai_pmo", "snapshot_focus": "none", "deep_intent": "none"},
|
||
thinking_mode="normal",
|
||
active_thinking_mode="normal",
|
||
deep_thinking=False,
|
||
enforce_scene_prompt_only=False,
|
||
deep_retrieval_steps=2,
|
||
deep_vector_top_k=3,
|
||
started=0.0,
|
||
message_intent="question",
|
||
mixed_signal_part=None,
|
||
anchor_resolution=FollowupAnchorResolution(
|
||
followup_mode="referential",
|
||
anchor_mode="no_anchor",
|
||
clarification_code="no_anchor_found",
|
||
),
|
||
consultant_mode_preference=None,
|
||
route_decision={
|
||
"route_mode": "internal",
|
||
"effective_source_policy": "internal",
|
||
"reply_contract": {"head": "内部知识库查询:", "tail_hint": None},
|
||
},
|
||
)
|
||
|
||
assert result["processing_stage"] == "retrieval"
|
||
assert service.clarification_payload is None
|
||
assert service.retrieval_kwargs["effective_q"] == rewritten_question
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_internal_model_config_fast_answer_bypasses_fast_path_gate_when_route_is_internal() -> None:
|
||
class _FakeContextCache:
|
||
async def get_system_snapshot(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
async def get_user_snapshot(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
class _FakeService(AdaptiveRAGAnswerMainlineMixin):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.context_cache = _FakeContextCache()
|
||
self.builder_calls = 0
|
||
self.retrieval_called = False
|
||
|
||
def _plan_query_runtime(self, **kwargs):
|
||
_ = kwargs
|
||
return SimpleNamespace(
|
||
thinking_mode="normal",
|
||
retrieval_answer_mode="retrieval_normal",
|
||
budget=SimpleNamespace(max_steps=2, vector_top_k=3),
|
||
allow_fast_path=False,
|
||
reason="consultant_mode",
|
||
)
|
||
|
||
def _build_followup_clarification_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _build_route_clarification_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _build_meta_followup_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _maybe_return_clarification_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _maybe_return_meta_followup_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _get_active_external_qa_context(self):
|
||
return {}
|
||
|
||
async def _build_internal_model_config_fast_answer(self, **kwargs):
|
||
_ = kwargs
|
||
self.builder_calls += 1
|
||
return {
|
||
"answer": "当前活动引擎是 qwen-plus。",
|
||
"sources": [{"source": "db", "label": "Integration[llm_engine]", "score": 0.99}],
|
||
}
|
||
|
||
async def _persist_fast_answer(self, **kwargs):
|
||
return {
|
||
"processing_stage": "fast_path",
|
||
"answer": kwargs["fast_result"]["answer"],
|
||
}
|
||
|
||
async def _build_retrieval_response(self, **kwargs):
|
||
_ = kwargs
|
||
self.retrieval_called = True
|
||
return {"processing_stage": "retrieval"}
|
||
|
||
async def _maybe_return_snapshot_direct_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _build_snapshot_direct_answer(self, *args, **kwargs):
|
||
_ = (args, kwargs)
|
||
return None
|
||
|
||
service = _FakeService()
|
||
result = await service._execute_base_answer_mainline(
|
||
clean_q="我们系统当前接的是哪个 codex 模型配置?",
|
||
asked_by=1,
|
||
asked_via="pmflow_manual",
|
||
project_id=None,
|
||
source_chat_id=None,
|
||
session_id=11,
|
||
turn_index=3,
|
||
context_session_id=11,
|
||
conversation=[],
|
||
state_row=None,
|
||
effective_q="我们系统当前接的是哪个 codex 模型配置?",
|
||
rewrite_meta={},
|
||
query_type="explain",
|
||
query_semantics={
|
||
"source_policy": "internal",
|
||
"effective_source_policy": "internal",
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["system_config", "model_routing"],
|
||
},
|
||
},
|
||
retrieval_domains=["db", "pmflow"],
|
||
scene_semantics={"scene_key": "ai_pmo", "snapshot_focus": "feature", "deep_intent": "none"},
|
||
thinking_mode="none",
|
||
active_thinking_mode="normal",
|
||
deep_thinking=False,
|
||
enforce_scene_prompt_only=False,
|
||
deep_retrieval_steps=2,
|
||
deep_vector_top_k=3,
|
||
started=0.0,
|
||
message_intent="system_configuration_inquiry",
|
||
mixed_signal_part=None,
|
||
consultant_mode_preference="informative",
|
||
route_decision={
|
||
"route_mode": "internal",
|
||
"effective_source_policy": "internal",
|
||
"reply_contract": {"head": "内部知识库查询:", "tail_hint": None},
|
||
"query_profile": {
|
||
"needs_private_context": True,
|
||
"needs_freshness": False,
|
||
"referenced_objects": ["system_config", "model_routing"],
|
||
},
|
||
},
|
||
)
|
||
|
||
assert result["processing_stage"] == "fast_path"
|
||
assert "qwen-plus" in result["answer"]
|
||
assert service.builder_calls == 1
|
||
assert service.retrieval_called is False
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_execute_mainline_blocks_internal_route_when_internal_context_packet_missing() -> None:
|
||
class _FakeContextCache:
|
||
async def get_system_snapshot(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
async def get_user_snapshot(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
class _FakeService(
|
||
AdaptiveRAGAnswerMainlineMixin,
|
||
AdaptiveRAGAnswerPayloadMixin,
|
||
):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.context_cache = _FakeContextCache()
|
||
self.persist_kwargs: dict[str, object] = {}
|
||
|
||
def _plan_query_runtime(self, **kwargs):
|
||
_ = kwargs
|
||
return SimpleNamespace(
|
||
thinking_mode="normal",
|
||
retrieval_answer_mode="retrieval_normal",
|
||
budget=SimpleNamespace(max_steps=2, vector_top_k=3),
|
||
allow_fast_path=True,
|
||
reason="deterministic_query_type",
|
||
)
|
||
|
||
def _build_followup_clarification_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _build_route_clarification_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _build_meta_followup_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _maybe_return_clarification_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _maybe_return_meta_followup_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _get_active_external_qa_context(self):
|
||
return {}
|
||
|
||
def _build_operation_contract(self, *, question: str, query_semantics):
|
||
_ = (question, query_semantics)
|
||
return {
|
||
"query_type": "existence",
|
||
"answer_contract": "yes_no_with_evidence",
|
||
"object_mentions": ["开票系统"],
|
||
}
|
||
|
||
async def _resolve_internal_target(self, **kwargs):
|
||
_ = kwargs
|
||
return {
|
||
"target_entity_type": "project",
|
||
"target_ref": "project:6",
|
||
"resolved_label": "自动开票系统",
|
||
"resolution_mode": "deterministic",
|
||
"confidence": 0.96,
|
||
"candidate_summary": [],
|
||
}
|
||
|
||
def _build_internal_query_context_packet(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _persist_fast_answer(self, **kwargs):
|
||
self.persist_kwargs = dict(kwargs)
|
||
return {
|
||
"processing_stage": "fast_path",
|
||
"answer": kwargs["fast_result"]["answer"],
|
||
"answer_mode": kwargs["answer_mode"],
|
||
"missing_fields": list(kwargs["fast_result"].get("missing_fields") or []),
|
||
}
|
||
|
||
async def _run_deterministic_query(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"deterministic query should be blocked: {kwargs}")
|
||
|
||
async def _build_internal_model_config_fast_answer(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"internal model config fast answer should be blocked: {kwargs}")
|
||
|
||
async def _try_glm_project_semantic_answer(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"glm semantic fast answer should be blocked: {kwargs}")
|
||
|
||
async def _build_retrieval_response(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"retrieval should be blocked: {kwargs}")
|
||
|
||
async def _maybe_return_snapshot_direct_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _build_snapshot_direct_answer(self, *args, **kwargs):
|
||
_ = (args, kwargs)
|
||
return None
|
||
|
||
service = _FakeService()
|
||
result = await service._execute_base_answer_mainline(
|
||
clean_q="帮我看看系统里面有没有开票系统。",
|
||
asked_by=1,
|
||
asked_via="pmflow_manual",
|
||
project_id=None,
|
||
source_chat_id=None,
|
||
session_id=11,
|
||
turn_index=3,
|
||
context_session_id=11,
|
||
conversation=[],
|
||
state_row=None,
|
||
effective_q="帮我看看系统里面有没有开票系统。",
|
||
rewrite_meta={},
|
||
query_type="explain",
|
||
query_semantics={
|
||
"source_policy": "internal",
|
||
"effective_source_policy": "internal",
|
||
"subject_focus": "project",
|
||
},
|
||
retrieval_domains=["db", "pmflow"],
|
||
scene_semantics={"scene_key": "ai_pmo", "snapshot_focus": "none", "deep_intent": "none"},
|
||
thinking_mode="normal",
|
||
active_thinking_mode="normal",
|
||
deep_thinking=False,
|
||
enforce_scene_prompt_only=False,
|
||
deep_retrieval_steps=2,
|
||
deep_vector_top_k=3,
|
||
started=0.0,
|
||
message_intent="question",
|
||
mixed_signal_part=None,
|
||
consultant_mode_preference=None,
|
||
route_decision={
|
||
"route_mode": "internal",
|
||
"effective_source_policy": "internal",
|
||
"reply_contract": {"head": "内部知识库查询:", "tail_hint": None},
|
||
},
|
||
)
|
||
|
||
assert result["processing_stage"] == "fast_path"
|
||
assert result["answer_mode"] == "clarify"
|
||
assert service.persist_kwargs["fast_result"]["reason_code"] == "missing_internal_context_packet"
|
||
assert service.persist_kwargs["fast_result"]["missing_fields"] == ["internal_context_packet"]
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_execute_mainline_blocks_internal_route_when_query_plan_contract_missing() -> None:
|
||
class _FakeContextCache:
|
||
async def get_system_snapshot(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
async def get_user_snapshot(self, **kwargs):
|
||
_ = kwargs
|
||
return {}
|
||
|
||
class _FakeService(
|
||
AdaptiveRAGAnswerMainlineMixin,
|
||
AdaptiveRAGAnswerPayloadMixin,
|
||
):
|
||
def __init__(self) -> None:
|
||
self.db = object()
|
||
self.tenant_id = 1
|
||
self.context_cache = _FakeContextCache()
|
||
self.persist_kwargs: dict[str, object] = {}
|
||
|
||
def _plan_query_runtime(self, **kwargs):
|
||
_ = kwargs
|
||
return SimpleNamespace(
|
||
thinking_mode="normal",
|
||
retrieval_answer_mode="retrieval_normal",
|
||
budget=SimpleNamespace(max_steps=2, vector_top_k=3),
|
||
allow_fast_path=True,
|
||
reason="deterministic_query_type",
|
||
)
|
||
|
||
def _build_followup_clarification_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _build_route_clarification_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _build_meta_followup_payload(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _maybe_return_clarification_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
async def _maybe_return_meta_followup_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _get_active_external_qa_context(self):
|
||
return {}
|
||
|
||
def _build_operation_contract(self, *, question: str, query_semantics):
|
||
_ = (question, query_semantics)
|
||
return {
|
||
"query_type": "existence",
|
||
"answer_contract": "yes_no_with_evidence",
|
||
"object_mentions": ["开票系统"],
|
||
}
|
||
|
||
async def _resolve_internal_target(self, **kwargs):
|
||
_ = kwargs
|
||
return {
|
||
"target_entity_type": "project",
|
||
"target_ref": "project:6",
|
||
"resolved_label": "自动开票系统",
|
||
"resolution_mode": "deterministic",
|
||
"confidence": 0.96,
|
||
"candidate_summary": [],
|
||
}
|
||
|
||
def _build_internal_query_context_packet(self, **kwargs):
|
||
payload = dict(kwargs)
|
||
return InternalContextPacket.model_validate(
|
||
{
|
||
"packet_version": "v1",
|
||
"question": str(payload.get("question") or ""),
|
||
"scope": {
|
||
"tenant_id": 1,
|
||
"project_id": None,
|
||
"session_id": 11,
|
||
"conversation_scope_key": None,
|
||
"reply_to_message_id": None,
|
||
"root_message_id": None,
|
||
},
|
||
"route_decision": dict(payload.get("route_decision") or {}),
|
||
"operation_contract": dict(payload.get("operation_contract") or {}),
|
||
"target_resolution": dict(payload.get("target_resolution") or {}),
|
||
"query_plan": {
|
||
"plan_version": "v2",
|
||
"query_type": "existence",
|
||
"entity_type": "project",
|
||
"answer_contract": "",
|
||
"target_ref": "project:6",
|
||
"filters": [{"field": "project_id", "operator": "equals", "value": 6}],
|
||
},
|
||
"executor_summary": {},
|
||
"verifier_summary": {},
|
||
"uncertainties": [],
|
||
}
|
||
)
|
||
|
||
async def _persist_fast_answer(self, **kwargs):
|
||
self.persist_kwargs = dict(kwargs)
|
||
return {
|
||
"processing_stage": "fast_path",
|
||
"answer": kwargs["fast_result"]["answer"],
|
||
"answer_mode": kwargs["answer_mode"],
|
||
"missing_fields": list(kwargs["fast_result"].get("missing_fields") or []),
|
||
}
|
||
|
||
async def _run_deterministic_query(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"deterministic query should be blocked: {kwargs}")
|
||
|
||
async def _build_internal_model_config_fast_answer(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"internal model config fast answer should be blocked: {kwargs}")
|
||
|
||
async def _try_glm_project_semantic_answer(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"glm semantic fast answer should be blocked: {kwargs}")
|
||
|
||
async def _build_retrieval_response(self, **kwargs): # pragma: no cover - should not be called
|
||
raise AssertionError(f"retrieval should be blocked: {kwargs}")
|
||
|
||
async def _maybe_return_snapshot_direct_response(self, **kwargs):
|
||
_ = kwargs
|
||
return None
|
||
|
||
def _build_snapshot_direct_answer(self, *args, **kwargs):
|
||
_ = (args, kwargs)
|
||
return None
|
||
|
||
service = _FakeService()
|
||
result = await service._execute_base_answer_mainline(
|
||
clean_q="帮我看看系统里面有没有开票系统。",
|
||
asked_by=1,
|
||
asked_via="pmflow_manual",
|
||
project_id=None,
|
||
source_chat_id=None,
|
||
session_id=11,
|
||
turn_index=3,
|
||
context_session_id=11,
|
||
conversation=[],
|
||
state_row=None,
|
||
effective_q="帮我看看系统里面有没有开票系统。",
|
||
rewrite_meta={},
|
||
query_type="explain",
|
||
query_semantics={
|
||
"source_policy": "internal",
|
||
"effective_source_policy": "internal",
|
||
"subject_focus": "project",
|
||
},
|
||
retrieval_domains=["db", "pmflow"],
|
||
scene_semantics={"scene_key": "ai_pmo", "snapshot_focus": "none", "deep_intent": "none"},
|
||
thinking_mode="normal",
|
||
active_thinking_mode="normal",
|
||
deep_thinking=False,
|
||
enforce_scene_prompt_only=False,
|
||
deep_retrieval_steps=2,
|
||
deep_vector_top_k=3,
|
||
started=0.0,
|
||
message_intent="question",
|
||
mixed_signal_part=None,
|
||
consultant_mode_preference=None,
|
||
route_decision={
|
||
"route_mode": "internal",
|
||
"effective_source_policy": "internal",
|
||
"reply_contract": {"head": "内部知识库查询:", "tail_hint": None},
|
||
},
|
||
)
|
||
|
||
assert result["processing_stage"] == "fast_path"
|
||
assert result["answer_mode"] == "clarify"
|
||
assert service.persist_kwargs["fast_result"]["reason_code"] == "missing_internal_query_plan_contract"
|
||
assert service.persist_kwargs["fast_result"]["missing_fields"] == ["answer_contract"]
|