"""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 []))