Files
X-Financial/server/src/app/services/scenes/scene_registry.py
caoxiaozhu 54356ba81a 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 会统一迁移
2026-06-25 15:09:16 +08:00

111 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)