refactor(server): scene 注册表骨架 + 统一门控管道设计文档
Phase 1 P1.1-P1.2:为后端门控收口提供声明式场景注册基础设施。 - 新建 scenes/ 目录:gate_rules(GateRule/SceneRoute 枚举)、scene_descriptor(SceneDescriptor dataclass)、scene_registry(SceneRegistry 单例) - 3 个场景迁入 descriptor:expense_application / reimbursement / query_travel_standard - __init__.py 的 bootstrap_scenes 在 import 时注册 + 运行时绑定 handler/builder/executor(解决循环 import) - 查询场景 priority=50 优先于 MODEL_ONLY 场景,确保规则匹配先于 LLM - 落地 UNIFIED_GATE_PIPELINE.md 架构文档:目标架构 / 验收标准(接入 O(1))/ 3 阶段迁移路径 - 76 passed,scene 注册表未破坏现有代码;与 intent_registry 暂时并存,P1.3-P1.8 会统一迁移
This commit is contained in:
110
server/src/app/services/scenes/scene_registry.py
Normal file
110
server/src/app/services/scenes/scene_registry.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
from app.services.scenes.gate_rules import GateRule, SceneRoute
|
||||
from app.services.scenes.scene_descriptor import SceneDescriptor
|
||||
|
||||
|
||||
class SceneRegistry:
|
||||
"""场景注册表单例。
|
||||
|
||||
所有场景在 import 时注册,门控/路由/执行/字段过滤全部从这里查询。
|
||||
gate_classify 节点是它的唯一消费者(单一决策点)。
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._scenes: dict[str, SceneDescriptor] = {}
|
||||
self._flow_to_scene: dict[str, str] = {}
|
||||
|
||||
# ---- 注册 ----
|
||||
|
||||
def register(self, descriptor: SceneDescriptor) -> SceneDescriptor:
|
||||
self._scenes[descriptor.scene_id] = descriptor
|
||||
if descriptor.flow_id:
|
||||
self._flow_to_scene[descriptor.flow_id] = descriptor.scene_id
|
||||
return descriptor
|
||||
|
||||
# ---- 查询 ----
|
||||
|
||||
def get(self, scene_id: str) -> SceneDescriptor | None:
|
||||
return self._scenes.get(str(scene_id or "").strip())
|
||||
|
||||
def all_scenes(self) -> list[SceneDescriptor]:
|
||||
return list(self._scenes.values())
|
||||
|
||||
def scenes_sorted_by_priority(self) -> list[SceneDescriptor]:
|
||||
"""按 priority 升序排列(数字小优先)。"""
|
||||
return sorted(self._scenes.values(), key=lambda s: s.priority)
|
||||
|
||||
def all_scene_ids(self) -> list[str]:
|
||||
return [s.scene_id for s in self._scenes.values()]
|
||||
|
||||
def all_assigned_agents(self) -> list[str]:
|
||||
return [s.assigned_agent for s in self._scenes.values() if s.assigned_agent]
|
||||
|
||||
def all_flow_ids(self) -> list[str]:
|
||||
return [s.flow_id for s in self._scenes.values() if s.flow_id]
|
||||
|
||||
def all_signal_keywords(self) -> set[str]:
|
||||
keywords: set[str] = set()
|
||||
for scene in self._scenes.values():
|
||||
keywords.update(scene.signal_keywords)
|
||||
return keywords
|
||||
|
||||
def all_side_effect_actions(self) -> set[str]:
|
||||
actions: set[str] = set()
|
||||
for scene in self._scenes.values():
|
||||
actions.update(scene.side_effect_actions)
|
||||
return actions
|
||||
|
||||
def all_noop_actions(self) -> set[str]:
|
||||
actions: set[str] = set()
|
||||
for scene in self._scenes.values():
|
||||
actions.update(scene.noop_actions)
|
||||
return actions
|
||||
|
||||
def resolve_scene_by_action(self, action_type: str) -> SceneDescriptor | None:
|
||||
normalized = str(action_type or "").strip()
|
||||
for scene in self._scenes.values():
|
||||
if normalized in scene.side_effect_actions or normalized in scene.noop_actions:
|
||||
return scene
|
||||
return None
|
||||
|
||||
def resolve_scene_by_flow(self, flow_id: str) -> SceneDescriptor | None:
|
||||
scene_id = self._flow_to_scene.get(str(flow_id or "").strip())
|
||||
return self.get(scene_id) if scene_id else None
|
||||
|
||||
def field_allowlist_for(
|
||||
self,
|
||||
scene_id: str,
|
||||
*,
|
||||
fallback: frozenset[str] | None = None,
|
||||
) -> frozenset[str]:
|
||||
scene = self.get(scene_id)
|
||||
if scene and scene.ontology_fields:
|
||||
return frozenset(scene.ontology_fields)
|
||||
return fallback or frozenset()
|
||||
|
||||
def resumable_scenes(self) -> list[SceneDescriptor]:
|
||||
"""返回所有声明了 can_resume=True 的场景。"""
|
||||
return [s for s in self._scenes.values() if s.can_resume]
|
||||
|
||||
def prompt_fragments(self) -> str:
|
||||
"""拼接所有场景的 prompt_fragment,供 system prompt 注入。"""
|
||||
fragments = [s.prompt_fragment for s in self._scenes.values() if s.prompt_fragment]
|
||||
return "".join(fragments)
|
||||
|
||||
def intent_summary(self) -> str:
|
||||
"""拼接场景列表摘要,供 system prompt 引用。"""
|
||||
fragments = [f"{s.scene_id}({s.label})" for s in self._scenes.values()]
|
||||
return "、".join(fragments) if fragments else "(暂无已注册场景)"
|
||||
|
||||
|
||||
# 全局单例
|
||||
REGISTRY = SceneRegistry()
|
||||
|
||||
|
||||
def register_scene(descriptor: SceneDescriptor) -> SceneDescriptor:
|
||||
"""注册场景到全局单例。"""
|
||||
return REGISTRY.register(descriptor)
|
||||
Reference in New Issue
Block a user