Protect business-review QA from low-tier routing regressions
This commit is contained in:
parent
5985f34a98
commit
6252cf1429
|
|
@ -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 场景回复必须强制两段格式输出:`[回答]` 与 `[已收到需求] ...,已入库处理`,禁止把需求段落并入问答段落
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {}),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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] = {}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue