PMFlow/backend/app/services/pmflow_ai_answer_renderer_r...

111 lines
4.2 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.

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