Protect business-review QA from low-tier routing regressions

This commit is contained in:
Yuuuu 2026-04-02 00:03:51 +08:00
parent 5985f34a98
commit 6252cf1429
9 changed files with 399 additions and 10 deletions

View File

@ -538,9 +538,11 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
- SSTC 发布验证清单必须优先消费 `derived_artifacts.verification_metrics`,其次才允许读取宪章 KPI;禁止再回退 3 条通用默认验收项 - SSTC 发布验证清单必须优先消费 `derived_artifacts.verification_metrics`,其次才允许读取宪章 KPI;禁止再回退 3 条通用默认验收项
- AdaptiveRAG `adaptive_rag_answer_normal` 空响应必须显式抛错并交由 high failover 处理;禁止在生成层直接回落为“补充问题细节后重试”之类的兜底回复 - AdaptiveRAG `adaptive_rag_answer_normal` 空响应必须显式抛错并交由 high failover 处理;禁止在生成层直接回落为“补充问题细节后重试”之类的兜底回复
- 用户可见问答链路(`function_name` 含 `answer/reply/qa`)禁止使用 `reasoning_content` 回退,避免思考过程外漏 - 用户可见问答链路(`function_name` 含 `answer/reply/qa`)禁止使用 `reasoning_content` 回退,避免思考过程外漏
- `integrations(type='model_routing')` 的 runtime tier override 不得把 `adaptive_rag_answer_normal` / `adaptive_rag_answer_deep` 这类用户可见 QA 主函数降级到 `low`;这两个函数必须优先服从 OpenClaw 的 high reasoning 规则,防止网页/飞书问答被后台 override 锁死在低档
- AdaptiveRAG 场景化 Prompt 选择器必须在 `mixin_deep_guard_answer.py` 组装 Prompt 前执行,并遵循固定优先级:成员负载 > 趋势(项目关联 external_trend / 通用 industry_trend) > mixed > AI PMO 入口 > 默认 RAG - AdaptiveRAG 场景化 Prompt 选择器必须在 `mixin_deep_guard_answer.py` 组装 Prompt 前执行,并遵循固定优先级:成员负载 > 趋势(项目关联 external_trend / 通用 industry_trend) > mixed > AI PMO 入口 > 默认 RAG
- 飞书私聊中的“新业务/需求/MVP/方案/怎么做”类探索问题必须先由 `workflow_semantic_planner` 标记为 `message_intent=business_review`,再统一落到 `PROMPT_AI_PMO_QA` 的五大口径评审场景;禁止继续复用 `PROMPT_FEISHU_DIRECT_QA` 生成类似普通 RAG 直答的结果 - 飞书私聊中的“新业务/需求/MVP/方案/怎么做”类探索问题必须先由 `workflow_semantic_planner` 标记为 `message_intent=business_review`,再统一落到 `PROMPT_AI_PMO_QA` 的五大口径评审场景;禁止继续复用 `PROMPT_FEISHU_DIRECT_QA` 生成类似普通 RAG 直答的结果
- 飞书私聊中的“答错了/不对/调用高级模型/用更强模型/重新回答”必须被识别为 `correction_escalation` 指令:优先复用上一轮上下文主题并强制深度回答,禁止把这类话术当成新的系统配置查询 - 飞书私聊中的“答错了/不对/调用高级模型/用更强模型/重新回答”必须被识别为 `correction_escalation` 指令:优先复用上一轮上下文主题并强制深度回答,禁止把这类话术当成新的系统配置查询
- `POST /rag/ask` 在 `rag_request_context` 或统一语义规划层命中 `business_review/correction_escalation` 时,必须强制留在 `query.read_only` 主链并进入对应 QA Prompt;禁止因为 LLM 意图分类把“分析/MVP/重答”误送到 `signal.create -> draft_pending_confirm`
- “高级模型是否启用/模型路由是否启用/当前是否高配”这类运行态问题,只有命中运行态配置、路由快照、引擎配置或真实调用记录时才允许下结论;若证据只有页面目录、代码图谱或功能清单,回复必须明确为“无法仅凭静态资料确认” - “高级模型是否启用/模型路由是否启用/当前是否高配”这类运行态问题,只有命中运行态配置、路由快照、引擎配置或真实调用记录时才允许下结论;若证据只有页面目录、代码图谱或功能清单,回复必须明确为“无法仅凭静态资料确认”
- 通用行业趋势问答(如“宠物行业最新动态”)不得注入数据库、项目历史、用户历史或外部实时搜索结果;只有显式带项目上下文时才允许走 `external_trend`,对“不要提项目/别提项目”这类否定语句不能误判为项目上下文 - 通用行业趋势问答(如“宠物行业最新动态”)不得注入数据库、项目历史、用户历史或外部实时搜索结果;只有显式带项目上下文时才允许走 `external_trend`,对“不要提项目/别提项目”这类否定语句不能误判为项目上下文
- mixed 场景回复必须强制两段格式输出:`[回答]` 与 `[已收到需求] ...,已入库处理`,禁止把需求段落并入问答段落 - mixed 场景回复必须强制两段格式输出:`[回答]` 与 `[已收到需求] ...,已入库处理`,禁止把需求段落并入问答段落

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import re
from .shared import * from .shared import *
def _normalize_noise_text(text: str) -> str: def _normalize_noise_text(text: str) -> str:

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import re
from .shared import * from .shared import *
from .helpers_inbox_filters import ( from .helpers_inbox_filters import (
_blocking_reason, _blocking_reason,

View File

@ -2,11 +2,26 @@ from __future__ import annotations
from .shared import * from .shared import *
from app.utils.thread_context import build_feishu_conversation_scope_key from app.utils.thread_context import build_feishu_conversation_scope_key
from app.services.ai_pmo.workflow_semantic_planner import WorkflowSemanticPlanner
from app.services.ai_pmo.workflow_semantic_planner.constants import (
QUERY_SCENE_BUSINESS_REVIEW,
QUERY_SCENE_CORRECTION_ESCALATION,
)
class KernelIngressFlowEntryMixin: class KernelIngressFlowEntryMixin:
_MIXED_HINT_CONNECTORS = ("顺便", "另外", "同时", "并且", "还有", "再问") _MIXED_HINT_CONNECTORS = ("顺便", "另外", "同时", "并且", "还有", "再问")
_MIXED_HINT_QUESTION = ("?", "?", "问下", "多少", "几个", "进展", "状态", "吗", "呢", "如何", "怎么", "有没有") _MIXED_HINT_QUESTION = ("?", "?", "问下", "多少", "几个", "进展", "状态", "吗", "呢", "如何", "怎么", "有没有")
_MIXED_HINT_REQUIREMENT = ("创建", "新建", "新增", "安排", "执行", "处理", "接入", "录入", "添加") _MIXED_HINT_REQUIREMENT = ("创建", "新建", "新增", "安排", "执行", "处理", "接入", "录入", "添加")
_RAG_QUERY_SCENE_KEYS = (
"message_intent",
"review_framework",
"consultant_mode_preference",
"force_thinking_mode",
)
_RAG_QUERY_SCENES = frozenset({
QUERY_SCENE_BUSINESS_REVIEW,
QUERY_SCENE_CORRECTION_ESCALATION,
})
@classmethod @classmethod
def _looks_like_mixed_candidate(cls, *, text: str) -> bool: def _looks_like_mixed_candidate(cls, *, text: str) -> bool:
@ -25,6 +40,129 @@ class KernelIngressFlowEntryMixin:
def _supports_mixed_sidecar(*, source_system: str) -> bool: def _supports_mixed_sidecar(*, source_system: str) -> bool:
return str(source_system or "").strip().lower() in {"feishu", "pmflow_manual", "ai_pmo_ui"} return str(source_system or "").strip().lower() in {"feishu", "pmflow_manual", "ai_pmo_ui"}
@classmethod
def _build_forced_query_scene_metadata(
cls,
*,
source_system: str,
message_text: str,
rag_request_context: Optional[dict[str, Any]],
semantic_bundle: Optional[dict[str, Any]],
) -> dict[str, Any]:
context = rag_request_context if isinstance(rag_request_context, dict) else {}
semantic_diagnostics = (
semantic_bundle.get("diagnostics")
if isinstance(semantic_bundle, dict) and isinstance(semantic_bundle.get("diagnostics"), dict)
else {}
)
semantic_primary_workflow = (
str(semantic_bundle.get("primary_workflow") or "").strip().lower()
if isinstance(semantic_bundle, dict)
else ""
)
derived = WorkflowSemanticPlanner._derive_query_scene_metadata(message_text)
scene_metadata: dict[str, Any] = {
key: value
for key, value in derived.items()
if key in cls._RAG_QUERY_SCENE_KEYS and value not in (None, "")
}
if str(source_system or "").strip().lower() == "feishu" and semantic_primary_workflow == "query_flow":
for key in cls._RAG_QUERY_SCENE_KEYS:
value = semantic_diagnostics.get(key)
if value in (None, ""):
continue
text = str(value).strip()
if text:
scene_metadata[key] = text.lower() if key != "review_framework" else text
for key in cls._RAG_QUERY_SCENE_KEYS:
value = context.get(key)
if value in (None, ""):
continue
text = str(value).strip()
if text:
scene_metadata[key] = text.lower() if key != "review_framework" else text
return scene_metadata
@classmethod
def _should_force_query_scene(
cls,
*,
source_system: str,
rag_request_context: Optional[dict[str, Any]],
semantic_bundle: Optional[dict[str, Any]],
scene_metadata: dict[str, Any],
) -> bool:
message_intent = str(scene_metadata.get("message_intent") or "").strip().lower()
if message_intent not in cls._RAG_QUERY_SCENES:
return False
if isinstance(rag_request_context, dict):
action_mode = str(rag_request_context.get("action_mode") or "auto").strip().lower() or "auto"
return action_mode not in {"write", "read_write"}
if str(source_system or "").strip().lower() == "feishu":
primary_workflow = (
str(semantic_bundle.get("primary_workflow") or "").strip().lower()
if isinstance(semantic_bundle, dict)
else ""
)
return primary_workflow == "query_flow"
return False
def _coerce_intent_for_query_scene(
self,
*,
intent: CanonicalIntent,
source_system: str,
message_text: str,
rag_request_context: Optional[dict[str, Any]],
semantic_bundle: Optional[dict[str, Any]],
world_model: dict[str, Any],
) -> CanonicalIntent:
scene_metadata = self._build_forced_query_scene_metadata(
source_system=source_system,
message_text=message_text,
rag_request_context=rag_request_context,
semantic_bundle=semantic_bundle,
)
if not self._should_force_query_scene(
source_system=source_system,
rag_request_context=rag_request_context,
semantic_bundle=semantic_bundle,
scene_metadata=scene_metadata,
):
return intent
project_id = _safe_int(intent.project_id) or _safe_int(world_model.get("resolved_project_id"))
semantic_bundle = WorkflowSemanticPlanner.plan_from_intent(
intent_name=INTENT_QUERY,
message_text=message_text,
source_system="ai_pmo_ingress",
workflow_route=ROUTE_QUERY_ANSWER,
project_id=project_id,
metadata={
"confidence": max(float(intent.confidence or 0.0), 0.86),
**scene_metadata,
},
).to_dict()
metadata = {
**(intent.metadata or {}),
**scene_metadata,
"context_source": "rag_request_context",
"workflow_route": ROUTE_QUERY_ANSWER,
"workflow_family": ROUTE_FAMILY_QUERY,
"workflow_action": ROUTE_ACTION_ANSWER,
"requires_confirmation": False,
"semantic_bundle": semantic_bundle,
}
pretty = re.sub(r"\s+", " ", str(message_text or "")).strip()
return CanonicalIntent(
name=INTENT_QUERY,
confidence=max(float(intent.confidence or 0.0), 0.86),
project_id=project_id,
signal_title="",
signal_summary=pretty[:300],
metadata=metadata,
)
async def _prepare_mixed_sidecar_context( async def _prepare_mixed_sidecar_context(
self, self,
*, *,
@ -112,7 +250,13 @@ class KernelIngressFlowEntryMixin:
message_sent_at: Optional[Any] = None, message_sent_at: Optional[Any] = None,
session_snapshot: Optional[dict[str, Any]] = None, session_snapshot: Optional[dict[str, Any]] = None,
thread_context: Optional[dict[str, Any]] = None, thread_context: Optional[dict[str, Any]] = None,
semantic_bundle: Optional[dict[str, Any]] = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
ingress_options: dict[str, Any] = {}
if isinstance(thread_context, dict) and thread_context:
ingress_options["thread_context"] = dict(thread_context)
if isinstance(semantic_bundle, dict) and semantic_bundle:
ingress_options["semantic_bundle"] = dict(semantic_bundle)
return await self.process_ingress( return await self.process_ingress(
source_system="feishu", source_system="feishu",
source_message_id=source_message_id, source_message_id=source_message_id,
@ -124,11 +268,7 @@ class KernelIngressFlowEntryMixin:
chat_type=chat_type, chat_type=chat_type,
message_sent_at=message_sent_at, message_sent_at=message_sent_at,
session_snapshot=session_snapshot, session_snapshot=session_snapshot,
ingress_options=( ingress_options=ingress_options or None,
{"thread_context": dict(thread_context)}
if isinstance(thread_context, dict) and thread_context
else None
),
) )
async def process_ingress( async def process_ingress(
@ -233,6 +373,16 @@ class KernelIngressFlowEntryMixin:
_log_perf("_build_world_model", stage_started_at) _log_perf("_build_world_model", stage_started_at)
self._schedule_portfolio_snapshot_persist(world_model=world_model) self._schedule_portfolio_snapshot_persist(world_model=world_model)
work_item.state = STATE_MODELED work_item.state = STATE_MODELED
rag_request_context = (
ingress_options.get("rag_request_context")
if isinstance(ingress_options, dict)
else None
)
semantic_bundle_override = (
ingress_options.get("semantic_bundle")
if isinstance(ingress_options, dict) and isinstance(ingress_options.get("semantic_bundle"), dict)
else None
)
stage_started_at = time.perf_counter() stage_started_at = time.perf_counter()
intent, intent_context_source = await self._resolve_intent( intent, intent_context_source = await self._resolve_intent(
message_text=clean_text, message_text=clean_text,
@ -242,6 +392,14 @@ class KernelIngressFlowEntryMixin:
session_snapshot=session_snapshot, session_snapshot=session_snapshot,
thread_context=thread_context, thread_context=thread_context,
) )
intent = self._coerce_intent_for_query_scene(
intent=intent,
source_system=source_system,
message_text=clean_text,
rag_request_context=rag_request_context,
semantic_bundle=semantic_bundle_override,
world_model=world_model,
)
intent = self._enrich_signal_source_intent_from_closure( intent = self._enrich_signal_source_intent_from_closure(
intent=intent, intent=intent,
message_text=clean_text, message_text=clean_text,
@ -252,11 +410,6 @@ class KernelIngressFlowEntryMixin:
**(intent.metadata or {}), **(intent.metadata or {}),
**mixed_meta, **mixed_meta,
} }
rag_request_context = (
ingress_options.get("rag_request_context")
if isinstance(ingress_options, dict)
else None
)
if isinstance(rag_request_context, dict): if isinstance(rag_request_context, dict):
intent.metadata = { intent.metadata = {
**(intent.metadata or {}), **(intent.metadata or {}),

View File

@ -3,6 +3,11 @@ from __future__ import annotations
from .shared import * from .shared import *
class LLMEngineResolutionCoreMixin: class LLMEngineResolutionCoreMixin:
_PROTECTED_QUERY_HIGH_FUNCTIONS = frozenset({
"adaptive_rag_answer_deep",
"adaptive_rag_answer_normal",
})
def _stub_response(self, prompt: str) -> str: def _stub_response(self, prompt: str) -> str:
_ = prompt # 保留签名,避免调用方改动 _ = prompt # 保留签名,避免调用方改动
return ( return (
@ -262,6 +267,12 @@ class LLMEngineResolutionCoreMixin:
if not override or not bool(override.get("enabled", True)): if not override or not bool(override.get("enabled", True)):
return None return None
tier_alias = _optional_tier_alias(override.get("tier_alias") or override.get("tier")) or "high" tier_alias = _optional_tier_alias(override.get("tier_alias") or override.get("tier")) or "high"
if tier_alias == "low" and fn in self._PROTECTED_QUERY_HIGH_FUNCTIONS:
_runtime_log(
"LLM路由",
f"函数覆盖低档降级已忽略:function={fn}, protected_reason=query_high_guard",
)
return None
config_bundle = load_model_config() config_bundle = load_model_config()
matched_rule = find_matching_rule( matched_rule = find_matching_rule(
fn, fn,

View File

@ -115,6 +115,12 @@ class SignalCaptureEntryMixin:
sync_reason = "short_confirmation" sync_reason = "short_confirmation"
if should_sync_process: if should_sync_process:
semantic_bundle = None
if str(sync_reason or "").strip().lower() == "semantic:query_flow":
semantic_bundle = WorkflowSemanticPlanner.plan_message(
message_text=cleaned_message,
source_system="feishu",
).to_dict()
self._emit_ai_think( self._emit_ai_think(
"capture_sync_high_accuracy", "capture_sync_high_accuracy",
trace_id=trace_id, trace_id=trace_id,
@ -140,6 +146,7 @@ class SignalCaptureEntryMixin:
message_sent_at=effective_message_sent_at, message_sent_at=effective_message_sent_at,
session_snapshot=effective_session_snapshot, session_snapshot=effective_session_snapshot,
thread_context=thread_context, thread_context=thread_context,
semantic_bundle=semantic_bundle,
) )
if self._is_non_requirement_message(cleaned_message) and not self._is_question_like(cleaned_message): if self._is_non_requirement_message(cleaned_message) and not self._is_question_like(cleaned_message):
@ -270,6 +277,7 @@ class SignalCaptureEntryMixin:
message_sent_at: Optional[Any] = None, message_sent_at: Optional[Any] = None,
session_snapshot: Optional[Dict[str, Any]] = None, session_snapshot: Optional[Dict[str, Any]] = None,
thread_context: Optional[Dict[str, Any]] = None, thread_context: Optional[Dict[str, Any]] = None,
semantic_bundle: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
pipeline_result = await kernel.process_feishu_message( pipeline_result = await kernel.process_feishu_message(
group_chat_id=group_chat_id, group_chat_id=group_chat_id,
@ -282,6 +290,7 @@ class SignalCaptureEntryMixin:
message_sent_at=message_sent_at, message_sent_at=message_sent_at,
session_snapshot=session_snapshot, session_snapshot=session_snapshot,
thread_context=thread_context, thread_context=thread_context,
semantic_bundle=semantic_bundle,
) )
if not bool(pipeline_result.get("handled")): if not bool(pipeline_result.get("handled")):
self._emit_ai_think( self._emit_ai_think(

View File

@ -5,6 +5,8 @@ import pytest
from fastapi import HTTPException from fastapi import HTTPException
from app.api.ai_pmo_api import routes_core from app.api.ai_pmo_api import routes_core
from app.api.ai_pmo_api import helpers_inbox_filters
from app.api.ai_pmo_api import helpers_inbox_payload
from app.api.ai_pmo_api.schemas import PMOProcessTextRequest from app.api.ai_pmo_api.schemas import PMOProcessTextRequest
from app.services.ai_pmo.consultant_dispatch import build_consultant_dispatch_contract from app.services.ai_pmo.consultant_dispatch import build_consultant_dispatch_contract
from app.services.ai_pmo.ai_pmo_kernel.mixins.intent.ai_pmo_kernel_mixins_intent_resolve import ( from app.services.ai_pmo.ai_pmo_kernel.mixins.intent.ai_pmo_kernel_mixins_intent_resolve import (
@ -290,6 +292,116 @@ def test_build_feishu_thread_key_prefers_actor_scope_without_anchor() -> None:
) == "feishu:chat-1:ou_dm_1" ) == "feishu:chat-1:ou_dm_1"
def test_low_value_closure_payload_helper_handles_summary_text() -> None:
assert helpers_inbox_filters._is_low_value_closure_payload(
{"closure_summary": "待补充核心痛点"}
) is False
def test_serialize_work_item_summary_handles_message_text_without_nameerror() -> None:
row = SimpleNamespace(
id=9,
source_system="pmflow_manual",
source_message_id="msg-9",
draft_content={},
state="received",
review_summary={},
action_bundle={},
intent="query.read_only",
project_id=None,
signal_id=None,
reply_template_key=None,
execution_result={},
draft_expires_at=None,
draft_version=0,
confirm_message_id=None,
created_at=None,
updated_at=None,
decision_level="portfolio",
)
ingest = SimpleNamespace(payload={"message_text": " 最近 状态 怎么样 "})
payload = helpers_inbox_payload._serialize_work_item_summary(
row=row,
ingest=ingest,
project=None,
)
assert payload["source_message_text"] == "最近 状态 怎么样"
def test_coerce_rag_business_review_scene_keeps_rag_ask_on_query_chain() -> None:
class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin):
def __init__(self) -> None:
self.tenant_id = 1
kernel = FakeKernel()
original_intent = mixin_entry.CanonicalIntent(
name="signal.create",
confidence=0.91,
project_id=None,
signal_title="大模型 API 聚合平台",
signal_summary="我现在要做一个大模型api聚合平台,你帮我分析下要怎么做,给出mvp需求",
metadata={"llm_function": "classify_intent_ai_pmo_kernel"},
)
coerced = kernel._coerce_intent_for_query_scene(
intent=original_intent,
source_system="pmflow_manual",
message_text="我现在要做一个大模型api聚合平台,你帮我分析下要怎么做,给出mvp需求",
rag_request_context={
"asked_via": "pmflow_manual",
"action_mode": "auto",
},
semantic_bundle=None,
world_model={},
)
assert coerced.name == "query.read_only"
assert coerced.metadata["message_intent"] == "business_review"
assert coerced.metadata["consultant_mode_preference"] == "clarify"
assert coerced.metadata["workflow_route"] == "query.answer"
semantic_bundle = coerced.metadata["semantic_bundle"]
assert semantic_bundle["diagnostics"]["message_intent"] == "business_review"
assert semantic_bundle["diagnostics"]["workflow_route"] == "query.answer"
def test_coerce_feishu_semantic_business_review_bundle_keeps_query_chain() -> None:
class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin):
def __init__(self) -> None:
self.tenant_id = 1
kernel = FakeKernel()
original_intent = mixin_entry.CanonicalIntent(
name="signal.create",
confidence=0.93,
project_id=None,
signal_title="大模型 API 聚合平台",
signal_summary="我现在要创建需求为一个大模型api聚合平台,你帮我分析下要怎么做,",
metadata={"llm_function": "classify_intent_ai_pmo_kernel"},
)
coerced = kernel._coerce_intent_for_query_scene(
intent=original_intent,
source_system="feishu",
message_text="我现在要创建需求为一个大模型api聚合平台,你帮我分析下要怎么做,",
rag_request_context=None,
semantic_bundle={
"primary_workflow": "query_flow",
"diagnostics": {
"message_intent": "business_review",
"review_framework": "five_lenses",
"consultant_mode_preference": "clarify",
},
},
world_model={},
)
assert coerced.name == "query.read_only"
assert coerced.metadata["message_intent"] == "business_review"
assert coerced.metadata["workflow_route"] == "query.answer"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_kernel_intent_resolve_inherits_recent_feishu_session_context_without_reply_anchor() -> None: async def test_kernel_intent_resolve_inherits_recent_feishu_session_context_without_reply_anchor() -> None:
captured: dict[str, object] = {} captured: dict[str, object] = {}

View File

@ -407,6 +407,66 @@ async def test_feishu_question_like_messages_use_sync_high_accuracy_path(monkeyp
assert calls["cache"] == 1 assert calls["cache"] == 1
@pytest.mark.asyncio
async def test_feishu_business_review_query_forwards_semantic_bundle_into_kernel(monkeypatch) -> None:
captured: dict[str, object] = {}
class _FakeKernel:
@staticmethod
def _build_source_message_id(*, source_system: str, source_message_id, channel_ref: str, sender: str, message_text: str):
_ = (source_system, source_message_id, channel_ref, sender, message_text)
return "msg-business-review"
def __init__(self, db, tenant_id: int):
_ = (db, tenant_id)
async def _get_or_create_session(self, **kwargs):
_ = kwargs
raise AssertionError("business review sync path should not create async queue session")
def _is_short_confirmation_message(self, **kwargs):
_ = kwargs
return False
async def process_feishu_message(self, **kwargs):
captured["kwargs"] = kwargs
return {
"handled": True,
"status": "answered",
"classification": "question",
"linked_project_ids": [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": "trace-br",
"direct_reply_required": True,
"direct_reply_text": "五大口径评审中。",
}
async def _noop_sync_recent_cache(self, **kwargs):
_ = (self, kwargs)
monkeypatch.setattr(mixin_capture_entry, "AIPMOKernelService", _FakeKernel)
monkeypatch.setattr(ProactiveSignalCaptureService, "_sync_feishu_recent_cache", _noop_sync_recent_cache)
service = ProactiveSignalCaptureService(object())
result = await service.capture_from_feishu(
group_chat_id="chat-br-1",
message_content="我现在要创建需求为一个大模型api聚合平台,你帮我分析下要怎么做,",
sender="测试用户",
tenant_id=1,
feishu_message_id="msg-business-review",
chat_type="group",
)
assert result["status"] == "answered"
kwargs = captured["kwargs"]
assert isinstance(kwargs, dict)
semantic_bundle = kwargs["semantic_bundle"]
assert semantic_bundle["primary_workflow"] == "query_flow"
assert semantic_bundle["diagnostics"]["message_intent"] == "business_review"
def test_capture_from_feishu_no_longer_references_legacy_fallback() -> None: def test_capture_from_feishu_no_longer_references_legacy_fallback() -> None:
source = inspect.getsource(mixin_capture_entry.SignalCaptureEntryMixin.capture_from_feishu) source = inspect.getsource(mixin_capture_entry.SignalCaptureEntryMixin.capture_from_feishu)

View File

@ -166,6 +166,44 @@ async def test_model_routing_override_is_tier_only_and_reads_last_execution(db_e
assert "api_env" not in mapping assert "api_env" not in mapping
@pytest.mark.asyncio
async def test_runtime_low_override_does_not_downgrade_protected_adaptive_rag_functions(db_env) -> None:
db_session, _maker = db_env
await ModelRoutingService.upsert_override(
db_session,
tenant_id=1,
function_name="adaptive_rag_answer_deep",
tier_alias="low",
updated_by=9,
)
low_engine = {
"id": "low-1",
"provider": "glm",
"model": "qwen-plus",
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"api_key": "low-key",
}
high_engine = {
"id": "high-1",
"provider": "openai",
"model": "GLM-5",
"base_url": "https://api.openai.com/v1",
"api_key": "high-key",
}
routed = await llm_service._resolve_function_override_engine(
db_session,
tenant_id=1,
function_name="adaptive_rag_answer_deep",
engines=[low_engine, high_engine],
active_engine=low_engine,
)
assert routed is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_model_routing_list_includes_current_business_entry_points(db_env) -> None: async def test_model_routing_list_includes_current_business_entry_points(db_env) -> None:
db_session, _maker = db_env db_session, _maker = db_env