111 lines
4.2 KiB
Python
111 lines
4.2 KiB
Python
"""Renderer selection registry for PMFlow AI turn runtime.
|
||
|
||
The registry chooses renderer candidates from canonical TurnState. It does not
|
||
inspect user text and it does not generate answers. Runtime-owned renderer
|
||
functions execute the selected candidates and may later be moved behind the
|
||
same keys without changing the turn loop.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from typing import Literal
|
||
|
||
from ..schemas.pmflow_ai_turn_runtime import PMFlowAITurnState
|
||
from .pmflow_ai_static_contract_registry import PMFlowAIStaticContractRegistry
|
||
|
||
|
||
RendererKey = Literal[
|
||
"static_contract",
|
||
"collection_snapshot",
|
||
"project_operating_packet",
|
||
"open_judgement",
|
||
"llm_state_compose",
|
||
"safe_fallback",
|
||
]
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class PMFlowAIAnswerRendererCandidate:
|
||
renderer_key: RendererKey
|
||
reason: str
|
||
priority: int
|
||
|
||
|
||
class PMFlowAIAnswerRendererRegistry:
|
||
"""Select deterministic and generative renderer candidates for a turn."""
|
||
|
||
def select_candidates(self, turn_state: PMFlowAITurnState) -> list[PMFlowAIAnswerRendererCandidate]:
|
||
candidates: list[PMFlowAIAnswerRendererCandidate] = []
|
||
if PMFlowAIStaticContractRegistry().resolve_for_turn_state(turn_state) is not None:
|
||
candidates.append(
|
||
PMFlowAIAnswerRendererCandidate(
|
||
renderer_key="static_contract",
|
||
reason="权威真源是静态产品合同,优先确定性表达。",
|
||
priority=10,
|
||
)
|
||
)
|
||
if _can_render_collection(turn_state):
|
||
candidates.append(
|
||
PMFlowAIAnswerRendererCandidate(
|
||
renderer_key="collection_snapshot",
|
||
reason="TurnState 已包含集合观察或集合 draft,可确定性展示列表。",
|
||
priority=20,
|
||
)
|
||
)
|
||
if _has_project_operating_packet(turn_state):
|
||
candidates.append(
|
||
PMFlowAIAnswerRendererCandidate(
|
||
renderer_key="project_operating_packet",
|
||
reason="TurnState 已包含项目运行态 packet,可确定性表达项目状态。",
|
||
priority=30,
|
||
)
|
||
)
|
||
if not list(turn_state.evidence_state.known_facts or []):
|
||
candidates.append(
|
||
PMFlowAIAnswerRendererCandidate(
|
||
renderer_key="open_judgement",
|
||
reason="当前没有可吸收 facts,尝试基于开放判断状态给出受控初判。",
|
||
priority=40,
|
||
)
|
||
)
|
||
candidates.extend(
|
||
[
|
||
PMFlowAIAnswerRendererCandidate(
|
||
renderer_key="llm_state_compose",
|
||
reason="使用 TurnState 进行受控合成。",
|
||
priority=90,
|
||
),
|
||
PMFlowAIAnswerRendererCandidate(
|
||
renderer_key="safe_fallback",
|
||
reason="无 renderer 产出时使用客户安全 fallback。",
|
||
priority=100,
|
||
),
|
||
]
|
||
)
|
||
return sorted(candidates, key=lambda item: item.priority)
|
||
|
||
|
||
def _can_render_collection(turn_state: PMFlowAITurnState) -> bool:
|
||
selected = turn_state.selected_frame
|
||
if selected is not None and selected.operation_kind == "list_collection":
|
||
return True
|
||
if str(turn_state.answer_state.answer_shape or "") == "fact_list":
|
||
return True
|
||
draft = dict(turn_state.customer_packet_draft or {})
|
||
if isinstance(draft.get("items"), list) or isinstance(draft.get("count"), int):
|
||
return True
|
||
return any(
|
||
observation.observation_type == "collection_snapshot"
|
||
and (bool(observation.items) or observation.count is not None)
|
||
for observation in list(turn_state.observations or [])
|
||
)
|
||
|
||
|
||
def _has_project_operating_packet(turn_state: PMFlowAITurnState) -> bool:
|
||
structured = dict(turn_state.evidence_state.structured_evidence or {})
|
||
if isinstance(structured.get("project_operating_evidence_packet"), dict):
|
||
return True
|
||
packets = structured.get("project_operating_evidence_packets")
|
||
return any(isinstance(packet, dict) for packet in list(packets or []))
|