659 lines
29 KiB
Python
659 lines
29 KiB
Python
"""Agent council review and operating plan compiler."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from ...schemas.pmflow_ai_operating_runtime import (
|
||
PMFlowAIAgentCouncilReview,
|
||
PMFlowAIAgentWorkProduct,
|
||
PMFlowAIConflictLedgerEntry,
|
||
PMFlowAIOperatingMission,
|
||
PMFlowAIOperatingPlan,
|
||
PMFlowAIProductObjectProjection,
|
||
PMFlowAIProjectWorldStatePacket,
|
||
clean_text,
|
||
)
|
||
|
||
|
||
class PMFlowAIAgentCouncilService:
|
||
"""Review specialist work products before compiling the single plan.
|
||
|
||
The council consumes structured work products and project world-state only.
|
||
It does not re-route by user wording or fixed question categories.
|
||
"""
|
||
|
||
def review_and_compile(
|
||
self,
|
||
*,
|
||
mission: PMFlowAIOperatingMission,
|
||
project_world_state: PMFlowAIProjectWorldStatePacket,
|
||
work_products: list[PMFlowAIAgentWorkProduct],
|
||
) -> tuple[PMFlowAIAgentCouncilReview, PMFlowAIOperatingPlan]:
|
||
critic = CrossAgentCritic()
|
||
compiler = OperatingPlanCompiler()
|
||
role_reviews = critic.review_roles(work_products)
|
||
risk_vetoes = critic.collect_risk_vetoes(work_products)
|
||
candidate_actions = compiler.collect_candidate_actions(
|
||
project_world_state=project_world_state,
|
||
work_products=work_products,
|
||
)
|
||
conflict_ledger = critic.find_conflicts(
|
||
role_reviews=role_reviews,
|
||
candidate_actions=candidate_actions,
|
||
risk_vetoes=risk_vetoes,
|
||
project_world_state=project_world_state,
|
||
)
|
||
priority_order = compiler.compile_priority_order(
|
||
candidate_actions=candidate_actions,
|
||
risk_vetoes=risk_vetoes,
|
||
)
|
||
resolved_conflicts = [_resolved_conflict(item) for item in conflict_ledger]
|
||
status = "passed"
|
||
if any(item.resolution == "blocked" for item in conflict_ledger):
|
||
status = "blocked"
|
||
elif conflict_ledger:
|
||
status = "passed_with_conflicts"
|
||
council_review = PMFlowAIAgentCouncilReview(
|
||
status=status,
|
||
role_reviews=role_reviews,
|
||
conflict_ledger=conflict_ledger,
|
||
resolved_conflicts=resolved_conflicts,
|
||
risk_vetoes=risk_vetoes,
|
||
priority_order=priority_order,
|
||
resolution_summary=_resolution_summary(
|
||
status=status,
|
||
priority_order=priority_order,
|
||
conflict_ledger=conflict_ledger,
|
||
risk_vetoes=risk_vetoes,
|
||
),
|
||
)
|
||
return council_review, compiler.compile_plan(
|
||
mission=mission,
|
||
project_world_state=project_world_state,
|
||
work_products=work_products,
|
||
council_review=council_review,
|
||
)
|
||
|
||
|
||
class CrossAgentCritic:
|
||
"""Detect disagreement, missing role output and risk vetoes."""
|
||
|
||
def review_roles(self, work_products: list[PMFlowAIAgentWorkProduct]) -> list[dict[str, Any]]:
|
||
reviews: list[dict[str, Any]] = []
|
||
for product in work_products:
|
||
verification = product.verification if isinstance(product.verification, dict) else {}
|
||
findings = [clean_text(item, limit=180) for item in list(verification.get("findings") or [])[:8]]
|
||
reviews.append(
|
||
{
|
||
"agent_id": product.agent_id,
|
||
"product_type": product.product_type,
|
||
"status": product.status,
|
||
"verification_status": clean_text(verification.get("status") or product.status, limit=80),
|
||
"findings": [item for item in findings if item],
|
||
"claim_count": len(list(product.claims or [])),
|
||
"recommendation_count": len(list(product.recommendations or [])),
|
||
"prepared_action_count": len(list(product.prepared_actions or [])),
|
||
"blocked_action_count": len(list(product.blocked_actions or [])),
|
||
}
|
||
)
|
||
return reviews
|
||
|
||
def collect_risk_vetoes(self, work_products: list[PMFlowAIAgentWorkProduct]) -> list[dict[str, Any]]:
|
||
vetoes: list[dict[str, Any]] = []
|
||
for product in work_products:
|
||
for item in list(product.blocked_actions or [])[:12]:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
title = clean_text(item.get("title"), limit=240)
|
||
if not title:
|
||
continue
|
||
vetoes.append(
|
||
{
|
||
"title": title,
|
||
"reason": clean_text(item.get("reason") or "该动作需要先解除风险或确认边界。", limit=500),
|
||
"source_agent": clean_text(item.get("source_agent") or product.agent_id, limit=80),
|
||
"target_ref": clean_text(item.get("target_ref"), limit=160),
|
||
"resolution": "blocked_until_confirmed_or_unblocked",
|
||
"requires_user_confirm": bool(item.get("requires_user_confirm", True)),
|
||
}
|
||
)
|
||
return _dedupe_dicts(vetoes, key="title")[:12]
|
||
|
||
def find_conflicts(
|
||
self,
|
||
*,
|
||
role_reviews: list[dict[str, Any]],
|
||
candidate_actions: list[dict[str, Any]],
|
||
risk_vetoes: list[dict[str, Any]],
|
||
project_world_state: PMFlowAIProjectWorldStatePacket,
|
||
) -> list[PMFlowAIConflictLedgerEntry]:
|
||
conflicts: list[PMFlowAIConflictLedgerEntry] = []
|
||
for review in role_reviews:
|
||
if review.get("status") == "blocked" or review.get("verification_status") == "blocked":
|
||
conflicts.append(
|
||
PMFlowAIConflictLedgerEntry(
|
||
conflict_type="role_output_blocked",
|
||
involved_agents=[clean_text(review.get("agent_id"), limit=80)],
|
||
summary=f"{review.get('product_type')} 未通过角色产物检查。",
|
||
resolution="blocked",
|
||
resolution_reason="该角色产物不能作为最终推进动作的直接依据,只能进入证据边界。",
|
||
severity="high",
|
||
source_refs=[clean_text(review.get("agent_id"), limit=80)],
|
||
)
|
||
)
|
||
veto_keys = {_action_key(item) for item in risk_vetoes if _action_key(item)}
|
||
for action in candidate_actions:
|
||
action_key = _action_key(action)
|
||
if action_key and action_key in veto_keys:
|
||
conflicts.append(
|
||
PMFlowAIConflictLedgerEntry(
|
||
conflict_type="risk_veto_conflict",
|
||
involved_agents=_dedupe_texts(
|
||
[
|
||
clean_text(action.get("source_agent"), limit=80),
|
||
clean_text(_veto_for_key(risk_vetoes, action_key).get("source_agent"), limit=80),
|
||
]
|
||
),
|
||
summary=f"建议动作“{action.get('title')}”被风险审查否决。",
|
||
resolution="blocked",
|
||
resolution_reason=clean_text(
|
||
_veto_for_key(risk_vetoes, action_key).get("reason")
|
||
or "风险审查要求先解除边界后再执行。",
|
||
limit=800,
|
||
),
|
||
severity="high",
|
||
source_refs=[clean_text(action.get("source_agent"), limit=80), "risk_vetoes"],
|
||
)
|
||
)
|
||
if len(candidate_actions) > 1:
|
||
first_agents = _dedupe_texts([clean_text(item.get("source_agent"), limit=80) for item in candidate_actions[:6]])
|
||
conflicts.append(
|
||
PMFlowAIConflictLedgerEntry(
|
||
conflict_type="priority_order_competition",
|
||
involved_agents=first_agents,
|
||
summary="多个专业 agent 同时提出下一步,必须收敛为一个优先级顺序。",
|
||
resolution="resolved",
|
||
resolution_reason="按风险否决、准备度、项目世界状态来源和角色责任编译为单一 priority_order。",
|
||
severity="medium",
|
||
source_refs=first_agents,
|
||
)
|
||
)
|
||
if project_world_state.remaining_gaps:
|
||
conflicts.append(
|
||
PMFlowAIConflictLedgerEntry(
|
||
conflict_type="evidence_gap_boundary",
|
||
involved_agents=["agent_council"],
|
||
summary="项目世界状态仍有未覆盖 facet,完整经营判断需要保留证据边界。",
|
||
resolution="accepted",
|
||
resolution_reason="缺口进入 evidence_boundary,不阻断只读诊断和准备类动作。",
|
||
severity="medium",
|
||
source_refs=[project_world_state.world_state_id],
|
||
)
|
||
)
|
||
return conflicts[:16]
|
||
|
||
|
||
class OperatingPlanCompiler:
|
||
"""Compile one ordered operating plan from council-reviewed products."""
|
||
|
||
def collect_candidate_actions(
|
||
self,
|
||
*,
|
||
project_world_state: PMFlowAIProjectWorldStatePacket,
|
||
work_products: list[PMFlowAIAgentWorkProduct],
|
||
) -> list[dict[str, Any]]:
|
||
actions: list[dict[str, Any]] = []
|
||
for product in work_products:
|
||
for item in list(product.recommendations or [])[:8]:
|
||
action = _normalize_action(item, source_agent=product.agent_id, action_kind="recommendation")
|
||
if action:
|
||
actions.append(action)
|
||
for item in list(product.prepared_actions or [])[:8]:
|
||
action = _normalize_action(item, source_agent=product.agent_id, action_kind="prepared_change")
|
||
if action:
|
||
actions.append(action)
|
||
actions.extend(_world_state_actions(project_world_state))
|
||
return _dedupe_dicts(actions, key="title")[:24]
|
||
|
||
def compile_priority_order(
|
||
self,
|
||
*,
|
||
candidate_actions: list[dict[str, Any]],
|
||
risk_vetoes: list[dict[str, Any]],
|
||
) -> list[dict[str, Any]]:
|
||
veto_keys = {_action_key(item) for item in risk_vetoes if _action_key(item)}
|
||
ranked: list[dict[str, Any]] = []
|
||
for index, action in enumerate(candidate_actions):
|
||
key = _action_key(action)
|
||
if key and key in veto_keys:
|
||
continue
|
||
ranked.append(
|
||
{
|
||
**action,
|
||
"priority_score": _priority_score(action=action, original_index=index),
|
||
"council_status": "selected",
|
||
}
|
||
)
|
||
ranked = sorted(ranked, key=lambda item: item.get("priority_score", 9999))
|
||
return [
|
||
{key: value for key, value in {**item, "priority_rank": index + 1}.items() if key != "priority_score"}
|
||
for index, item in enumerate(ranked[:8])
|
||
]
|
||
|
||
def compile_plan(
|
||
self,
|
||
*,
|
||
mission: PMFlowAIOperatingMission,
|
||
project_world_state: PMFlowAIProjectWorldStatePacket,
|
||
work_products: list[PMFlowAIAgentWorkProduct],
|
||
council_review: PMFlowAIAgentCouncilReview,
|
||
) -> PMFlowAIOperatingPlan:
|
||
priority_order = list(council_review.priority_order or [])
|
||
prepared_changes = _prepared_changes_from_priority(priority_order=priority_order, work_products=work_products)
|
||
blocked_actions = _blocked_actions_from_review(council_review=council_review, work_products=work_products)
|
||
evidence_boundary = _evidence_boundary(
|
||
project_world_state=project_world_state,
|
||
work_products=work_products,
|
||
council_review=council_review,
|
||
)
|
||
product_object_projections = ProductObjectProjectionCompiler().compile(
|
||
mission=mission,
|
||
project_world_state=project_world_state,
|
||
work_products=work_products,
|
||
priority_order=priority_order,
|
||
evidence_boundary=evidence_boundary,
|
||
)
|
||
return PMFlowAIOperatingPlan(
|
||
strategic_read=_strategic_read_from_products(
|
||
project_world_state=project_world_state,
|
||
work_products=work_products,
|
||
fallback=mission.desired_outcome,
|
||
),
|
||
highest_leverage_move=_highest_leverage_move(priority_order=priority_order, fallback=mission.desired_outcome),
|
||
next_cycle_plan=priority_order[:6],
|
||
safe_suggestions=[item for item in priority_order[:6] if item.get("action_kind") != "prepared_change"],
|
||
prepared_changes=prepared_changes[:8],
|
||
requires_confirmation=[
|
||
{
|
||
"title": "确认是否按 Agent Council 编译后的优先级继续准备项目动作。",
|
||
"requires_user_confirm": True,
|
||
"source": "agent_council",
|
||
"priority_order_ref": council_review.review_id,
|
||
}
|
||
],
|
||
blocked_actions=blocked_actions[:12],
|
||
evidence_boundary=evidence_boundary,
|
||
council_review=council_review.model_dump(mode="json", exclude_none=True),
|
||
conflict_ledger=[item.model_dump(mode="json", exclude_none=True) for item in council_review.conflict_ledger],
|
||
resolved_conflicts=list(council_review.resolved_conflicts or []),
|
||
risk_vetoes=list(council_review.risk_vetoes or []),
|
||
priority_order=priority_order,
|
||
product_object_projections=[item.model_dump(mode="json", exclude_none=True) for item in product_object_projections],
|
||
)
|
||
|
||
|
||
class ProductObjectProjectionCompiler:
|
||
"""Compile specialist artifacts into PMFlow product object candidates."""
|
||
|
||
def compile(
|
||
self,
|
||
*,
|
||
mission: PMFlowAIOperatingMission,
|
||
project_world_state: PMFlowAIProjectWorldStatePacket,
|
||
work_products: list[PMFlowAIAgentWorkProduct],
|
||
priority_order: list[dict[str, Any]],
|
||
evidence_boundary: list[str],
|
||
) -> list[PMFlowAIProductObjectProjection]:
|
||
research_product = _product_for_type(work_products, "ResearchNote")
|
||
research_ready = _research_ready(research_product)
|
||
preferred_type = _projection_type_for_deliverable(mission.deliverable_type)
|
||
if mission.stage == "research" and not research_ready:
|
||
preferred_type = "ResearchNote"
|
||
primary_product = _primary_product_for_projection(work_products=work_products, object_type=preferred_type)
|
||
source_agents = _dedupe_texts([item.agent_id for item in work_products if item.status in {"ready", "draft", "needs_followup"}])
|
||
evidence_refs = _dedupe_texts([ref for product in work_products for ref in list(product.evidence_refs or [])])
|
||
title = _projection_title(mission=mission, object_type=preferred_type)
|
||
summary = _projection_summary(mission=mission, product=primary_product, priority_order=priority_order)
|
||
fields = {
|
||
"mission_id": mission.mission_id,
|
||
"stage": mission.stage,
|
||
"desired_business_outcome": mission.desired_business_outcome or mission.desired_outcome,
|
||
"priority_order": priority_order[:6],
|
||
"source_work_products": [
|
||
{
|
||
"agent_id": product.agent_id,
|
||
"product_type": product.product_type,
|
||
"status": product.status,
|
||
"summary": product.summary,
|
||
}
|
||
for product in work_products[:12]
|
||
],
|
||
"structured_output": primary_product.structured_output if primary_product is not None else {},
|
||
"remaining_boundaries": evidence_boundary[:8],
|
||
}
|
||
status = "ready_for_review" if summary and source_agents and (preferred_type != "ResearchNote" or research_ready) else "draft"
|
||
return [
|
||
PMFlowAIProductObjectProjection(
|
||
object_type=preferred_type,
|
||
title=title,
|
||
summary=summary,
|
||
source_agents=source_agents,
|
||
target_refs=list(project_world_state.project_refs or mission.project_scope.object_refs or []),
|
||
evidence_refs=evidence_refs[:16],
|
||
fields=fields,
|
||
action_ladder_level=mission.action_ladder_level,
|
||
confirmation_required=True,
|
||
status=status,
|
||
blocked_boundaries=evidence_boundary[:8] if status != "ready_for_review" else [],
|
||
next_cycle_policy=mission.next_cycle_policy,
|
||
)
|
||
]
|
||
|
||
|
||
def _normalize_action(item: dict[str, Any], *, source_agent: str, action_kind: str) -> dict[str, Any]:
|
||
if not isinstance(item, dict):
|
||
return {}
|
||
title = clean_text(item.get("title") or item.get("display_label"), limit=240)
|
||
if not title:
|
||
return {}
|
||
return {
|
||
"title": title,
|
||
"reason": clean_text(item.get("reason") or item.get("summary") or "", limit=500),
|
||
"action_type": clean_text(item.get("action_type") or action_kind, limit=100),
|
||
"action_kind": action_kind,
|
||
"source_agent": clean_text(item.get("source_agent") or source_agent, limit=80),
|
||
"target_ref": clean_text(item.get("target_ref"), limit=160),
|
||
"requires_user_confirm": bool(item.get("requires_user_confirm", True)),
|
||
}
|
||
|
||
|
||
def _world_state_actions(project_world_state: PMFlowAIProjectWorldStatePacket) -> list[dict[str, Any]]:
|
||
actions: list[dict[str, Any]] = []
|
||
for packet in list(project_world_state.project_operating_evidence_packets or [])[:8]:
|
||
if not isinstance(packet, dict):
|
||
continue
|
||
for item in list(packet.get("safe_next_actions") or [])[:8]:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
title = clean_text(item.get("display_label") or item.get("title"), limit=240)
|
||
if title:
|
||
actions.append(
|
||
{
|
||
"title": title,
|
||
"reason": clean_text(item.get("reason") or "来自项目世界状态的可推进动作。", limit=500),
|
||
"action_type": "prepare_from_project_world_state",
|
||
"action_kind": "world_state_action",
|
||
"source_agent": "project_world_state",
|
||
"target_ref": clean_text(item.get("target_ref") or packet.get("project_ref"), limit=160),
|
||
"requires_user_confirm": True,
|
||
}
|
||
)
|
||
return _dedupe_dicts(actions, key="title")[:12]
|
||
|
||
|
||
def _priority_score(*, action: dict[str, Any], original_index: int) -> int:
|
||
role_priority = {
|
||
"project_world_state": 10,
|
||
"pm_breakdown_agent": 20,
|
||
"product_strategy_agent": 30,
|
||
"mvp_scope_agent": 40,
|
||
"idea_incubation_agent": 45,
|
||
"market_research_agent": 50,
|
||
"competitor_agent": 55,
|
||
"tech_architect_agent": 60,
|
||
"qa_risk_agent": 70,
|
||
"growth_agent": 80,
|
||
"monetization_agent": 85,
|
||
"learning_agent": 90,
|
||
}
|
||
kind_priority = {
|
||
"world_state_action": 0,
|
||
"prepared_change": 5,
|
||
"recommendation": 10,
|
||
}
|
||
return (
|
||
int(kind_priority.get(clean_text(action.get("action_kind"), limit=80), 20))
|
||
+ int(role_priority.get(clean_text(action.get("source_agent"), limit=80), 90))
|
||
+ original_index
|
||
)
|
||
|
||
|
||
def _prepared_changes_from_priority(
|
||
*,
|
||
priority_order: list[dict[str, Any]],
|
||
work_products: list[PMFlowAIAgentWorkProduct],
|
||
) -> list[dict[str, Any]]:
|
||
prepared = []
|
||
priority_titles = {clean_text(item.get("title"), limit=240).lower() for item in priority_order}
|
||
for product in work_products:
|
||
for item in list(product.prepared_actions or [])[:8]:
|
||
title = clean_text(item.get("title"), limit=240)
|
||
if title and title.lower() in priority_titles:
|
||
prepared.append({**item, "source_agent": clean_text(item.get("source_agent") or product.agent_id, limit=80)})
|
||
for item in priority_order[:8]:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
if item.get("action_kind") not in {"world_state_action", "prepared_change"}:
|
||
continue
|
||
title = clean_text(item.get("title"), limit=240)
|
||
if not title:
|
||
continue
|
||
prepared.append(
|
||
{
|
||
"title": title,
|
||
"action_type": clean_text(item.get("action_type") or "prepare_task_change", limit=100),
|
||
"target_ref": clean_text(item.get("target_ref"), limit=160),
|
||
"requires_user_confirm": True,
|
||
"source_agent": clean_text(item.get("source_agent") or "agent_council", limit=80),
|
||
}
|
||
)
|
||
return _dedupe_dicts(prepared, key="title")
|
||
|
||
|
||
def _blocked_actions_from_review(
|
||
*,
|
||
council_review: PMFlowAIAgentCouncilReview,
|
||
work_products: list[PMFlowAIAgentWorkProduct],
|
||
) -> list[dict[str, Any]]:
|
||
rows: list[dict[str, Any]] = [
|
||
{
|
||
**item,
|
||
"source": "agent_council_risk_veto",
|
||
"council_status": "blocked",
|
||
}
|
||
for item in list(council_review.risk_vetoes or [])
|
||
if isinstance(item, dict)
|
||
]
|
||
for product in work_products:
|
||
for item in list(product.blocked_actions or [])[:8]:
|
||
if isinstance(item, dict):
|
||
rows.append({**item, "source_agent": clean_text(item.get("source_agent") or product.agent_id, limit=80)})
|
||
return _dedupe_dicts(rows, key="title")
|
||
|
||
|
||
def _evidence_boundary(
|
||
*,
|
||
project_world_state: PMFlowAIProjectWorldStatePacket,
|
||
work_products: list[PMFlowAIAgentWorkProduct],
|
||
council_review: PMFlowAIAgentCouncilReview,
|
||
) -> list[str]:
|
||
boundaries: list[str] = []
|
||
for conflict in list(council_review.conflict_ledger or [])[:8]:
|
||
if conflict.resolution == "blocked":
|
||
boundaries.append(f"会审阻断:{conflict.summary} {conflict.resolution_reason}")
|
||
else:
|
||
boundaries.append(f"会审边界:{conflict.summary} {conflict.resolution_reason}")
|
||
for product in work_products:
|
||
verification = product.verification if isinstance(product.verification, dict) else {}
|
||
for item in list(verification.get("findings") or [])[:4]:
|
||
text = clean_text(item, limit=180)
|
||
if text:
|
||
boundaries.append(f"{product.product_type} 验证边界:{text}")
|
||
if project_world_state.status in {"missing_scope", "unavailable"}:
|
||
boundaries.append("未读取到可安全绑定的项目世界状态,本轮不能把空间概览当作项目推进依据。")
|
||
for gap in list(project_world_state.remaining_gaps or [])[:8]:
|
||
boundaries.append(f"项目世界状态缺口:{gap}")
|
||
if not boundaries:
|
||
boundaries.append(f"已基于 Agent Council 会审和项目世界状态 {project_world_state.world_state_id} 编译计划。")
|
||
return _dedupe_texts(boundaries)[:12]
|
||
|
||
|
||
def _strategic_read_from_products(
|
||
*,
|
||
project_world_state: PMFlowAIProjectWorldStatePacket,
|
||
work_products: list[PMFlowAIAgentWorkProduct],
|
||
fallback: str,
|
||
) -> str:
|
||
strategist = next((item for item in work_products if item.agent_id == "product_strategy_agent"), None)
|
||
output = strategist.structured_output if strategist is not None and isinstance(strategist.structured_output, dict) else {}
|
||
current_read = clean_text(output.get("current_read"), limit=900)
|
||
highest = clean_text(output.get("highest_leverage_move"), limit=300)
|
||
if current_read or highest:
|
||
return f"{project_world_state.primary_project_ref or '当前项目'}:{current_read}{';最高杠杆动作:' + highest if highest else ''}"[:1200]
|
||
summaries = ";".join(clean_text(product.summary, limit=300) for product in work_products[:3] if product.summary)
|
||
return summaries[:1200] or clean_text(fallback, limit=1200)
|
||
|
||
|
||
def _highest_leverage_move(*, priority_order: list[dict[str, Any]], fallback: str) -> str:
|
||
first = next((clean_text(item.get("title"), limit=1200) for item in priority_order if isinstance(item, dict)), "")
|
||
return first or clean_text(fallback, limit=1200)
|
||
|
||
|
||
def _projection_type_for_deliverable(deliverable_type: str) -> str:
|
||
mapping = {
|
||
"idea_seed": "IdeaSeed",
|
||
"research_note": "ResearchNote",
|
||
"opportunity_brief": "OpportunityBrief",
|
||
"product_hypothesis": "ProductHypothesis",
|
||
"mvp_option": "MVPOption",
|
||
"requirement_draft": "RequirementDraft",
|
||
"changeset_draft": "ChangeSetDraft",
|
||
"task_draft": "TaskDraft",
|
||
"execution_package": "ExecutionPackage",
|
||
"qa_plan": "QAPlan",
|
||
"release_signal": "ReleaseSignal",
|
||
"growth_experiment": "GrowthExperiment",
|
||
"revenue_signal": "RevenueSignal",
|
||
"operating_decision": "OpportunityBrief",
|
||
}
|
||
return mapping.get(clean_text(deliverable_type, limit=80), "OpportunityBrief")
|
||
|
||
|
||
def _primary_product_for_projection(*, work_products: list[PMFlowAIAgentWorkProduct], object_type: str) -> PMFlowAIAgentWorkProduct | None:
|
||
return next((item for item in work_products if item.product_type == object_type), None) or next(
|
||
(item for item in work_products if item.agent_id == "product_strategy_agent"),
|
||
None,
|
||
) or (work_products[0] if work_products else None)
|
||
|
||
|
||
def _product_for_type(work_products: list[PMFlowAIAgentWorkProduct], product_type: str) -> PMFlowAIAgentWorkProduct | None:
|
||
return next((item for item in work_products if item.product_type == product_type), None)
|
||
|
||
|
||
def _research_ready(product: PMFlowAIAgentWorkProduct | None) -> bool:
|
||
if product is None or product.status != "ready":
|
||
return False
|
||
output = product.structured_output if isinstance(product.structured_output, dict) else {}
|
||
state = output.get("evidence_state") if isinstance(output.get("evidence_state"), dict) else {}
|
||
return bool(state.get("ready_for_opportunity_brief"))
|
||
|
||
|
||
def _projection_title(*, mission: PMFlowAIOperatingMission, object_type: str) -> str:
|
||
goal = clean_text(mission.original_user_input or mission.mission_goal, limit=120)
|
||
if object_type == "OpportunityBrief":
|
||
return f"机会简报:{goal}"[:240] if goal else "机会简报"
|
||
if object_type == "ResearchNote":
|
||
return f"研究记录:{goal}"[:240] if goal else "研究记录"
|
||
return f"{object_type}:{goal}"[:240] if goal else object_type
|
||
|
||
|
||
def _projection_summary(
|
||
*,
|
||
mission: PMFlowAIOperatingMission,
|
||
product: PMFlowAIAgentWorkProduct | None,
|
||
priority_order: list[dict[str, Any]],
|
||
) -> str:
|
||
product_summary = clean_text(product.summary if product is not None else "", limit=700)
|
||
if product is not None and product.product_type == "ResearchNote":
|
||
output = product.structured_output if isinstance(product.structured_output, dict) else {}
|
||
gaps = [clean_text(item, limit=120) for item in list(output.get("research_gaps") or [])[:3] if clean_text(item, limit=120)]
|
||
plan_count = len(list(output.get("source_plan") or []))
|
||
if product.status != "ready":
|
||
return (
|
||
product_summary
|
||
or f"先进入研究补证:已生成 {plan_count} 条来源补齐路径"
|
||
+ (f",还缺:{';'.join(gaps)}" if gaps else "。")
|
||
)[:1200]
|
||
lead = clean_text(priority_order[0].get("title"), limit=240) if priority_order else ""
|
||
outcome = clean_text(mission.desired_business_outcome or mission.desired_outcome, limit=500)
|
||
parts = [item for item in (product_summary, f"第一优先级:{lead}" if lead else "", outcome) if item]
|
||
return ";".join(parts)[:1200]
|
||
|
||
|
||
def _resolved_conflict(conflict: PMFlowAIConflictLedgerEntry) -> dict[str, Any]:
|
||
return {
|
||
"conflict_id": conflict.conflict_id,
|
||
"conflict_type": conflict.conflict_type,
|
||
"resolution": conflict.resolution,
|
||
"resolution_reason": conflict.resolution_reason,
|
||
"severity": conflict.severity,
|
||
}
|
||
|
||
|
||
def _resolution_summary(
|
||
*,
|
||
status: str,
|
||
priority_order: list[dict[str, Any]],
|
||
conflict_ledger: list[PMFlowAIConflictLedgerEntry],
|
||
risk_vetoes: list[dict[str, Any]],
|
||
) -> str:
|
||
lead = clean_text(priority_order[0].get("title"), limit=240) if priority_order else ""
|
||
conflict_count = len(conflict_ledger)
|
||
veto_count = len(risk_vetoes)
|
||
if status == "blocked":
|
||
return f"会审发现 {veto_count} 个风险否决和 {conflict_count} 个冲突,已阻断不可直接执行动作;可推进项按单一优先级保留。"
|
||
if lead:
|
||
return f"会审完成,已把 {conflict_count} 个冲突收敛为单一优先级;第一优先级是“{lead}”。"
|
||
return "会审完成,但当前没有足够可推进动作,只能保留证据边界和确认项。"
|
||
|
||
|
||
def _veto_for_key(vetoes: list[dict[str, Any]], key: str) -> dict[str, Any]:
|
||
return next((item for item in vetoes if _action_key(item) == key), {})
|
||
|
||
|
||
def _action_key(item: dict[str, Any]) -> str:
|
||
return clean_text(item.get("title") or item.get("display_label"), limit=240).lower()
|
||
|
||
|
||
def _dedupe_dicts(rows: list[dict[str, Any]], *, key: str) -> list[dict[str, Any]]:
|
||
seen: set[str] = set()
|
||
out: list[dict[str, Any]] = []
|
||
for item in rows:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
value = clean_text(item.get(key), limit=240)
|
||
normalized = value.lower()
|
||
if not value or normalized in seen:
|
||
continue
|
||
seen.add(normalized)
|
||
out.append(item)
|
||
return out
|
||
|
||
|
||
def _dedupe_texts(rows: list[str]) -> list[str]:
|
||
out: list[str] = []
|
||
for item in rows:
|
||
text = clean_text(item, limit=180)
|
||
if text and text not in out:
|
||
out.append(text)
|
||
return out
|
||
|
||
|
||
__all__ = [
|
||
"CrossAgentCritic",
|
||
"OperatingPlanCompiler",
|
||
"ProductObjectProjectionCompiler",
|
||
"PMFlowAIAgentCouncilService",
|
||
]
|