PMFlow/backend/app/services/pmflow_ai_project_execution...

861 lines
34 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.

"""Project execution observation adapter for PMFlow AI.
This module converts PMFlow OS truth/execution projections into customer-safe
evidence facts. It is intentionally read-only and does not decide the final
answer shape.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from ..schemas.project_operating_evidence import (
ProjectOperatingEvidenceFacet,
ProjectOperatingEvidencePacket,
ProjectOperatingEvidenceRef,
ProjectOperatingSuggestedAction,
)
from ..schemas.pmflow_ai_turn_runtime import PMFlowAIEvidenceFinding
@dataclass(frozen=True)
class ProjectOperatingEvidenceReaderInput:
project_id: int
project_title: str
project_status: str
project_stage: str
loop_stage: str
truth: dict[str, Any]
execution_summary: dict[str, Any]
project: dict[str, Any]
execution: dict[str, Any]
acceptance: dict[str, Any]
release_gate: dict[str, Any]
quality_contract: dict[str, Any]
quality_summary: dict[str, Any]
task_counts: dict[str, Any]
active_tasks: list[dict[str, Any]]
current_task: dict[str, Any]
evidence_ref: ProjectOperatingEvidenceRef
def build_project_operating_evidence_packet(
*,
project_id: int,
title: str,
status: str,
truth_payload: dict[str, Any] | None,
execution_payload: dict[str, Any] | None,
fallback_task_counts: dict[str, Any] | None,
object_binding: dict[str, Any] | None = None,
) -> ProjectOperatingEvidencePacket:
"""Build a structured read-only operating packet for one project."""
truth = _dict(truth_payload)
execution_summary = _dict(execution_payload)
project = _dict(truth.get("project"))
execution = _dict(execution_summary.get("execution"))
acceptance = _dict(execution_summary.get("acceptance"))
release_gate = _dict(execution_summary.get("releaseGate"))
quality_contract = _dict(execution_summary.get("qualityContract"))
quality_summary = _dict(quality_contract.get("summary"))
task_counts = _dict(fallback_task_counts)
active_tasks = _active_tasks(execution_summary)
current_task = _current_task(execution=execution, execution_summary=execution_summary)
project_title = _text(project.get("name") or project.get("title") or title or f"项目 {project_id}", 160)
project_status = _text(project.get("status") or status, 80)
project_stage = _text(project.get("stage") or truth.get("stage"), 120)
loop_stage = _text(truth.get("loopStage"), 120)
ref = ProjectOperatingEvidenceRef(
ref_type="project",
ref_id=f"project:{project_id}",
display_label=project_title,
source_surface="PMFlow OS",
)
reader_input = ProjectOperatingEvidenceReaderInput(
project_id=project_id,
project_title=project_title,
project_status=project_status,
project_stage=project_stage,
loop_stage=loop_stage,
truth=truth,
execution_summary=execution_summary,
project=project,
execution=execution,
acceptance=acceptance,
release_gate=release_gate,
quality_contract=quality_contract,
quality_summary=quality_summary,
task_counts=task_counts,
active_tasks=active_tasks,
current_task=current_task,
evidence_ref=ref,
)
command_state = _read_command_state(reader_input)
workbench_state = _read_workbench_state(reader_input)
decision_review_state = _read_decision_review_state(reader_input)
execution_state = _read_execution_state(reader_input)
quality_test_state = _read_quality_test_state(reader_input)
release_growth_state = _read_release_growth_state(reader_input)
team_state = _read_team_state(reader_input)
project_memory_state = _read_project_memory_state(reader_input)
risk_blocker_state = _read_risk_blocker_state(reader_input)
recent_timeline_state = _read_recent_timeline_state(reader_input)
next_actions = _unique_texts(list(release_gate.get("nextActions") or []), limit=6)
blockers = _project_blockers(acceptance=acceptance, release_gate=release_gate, quality_contract=quality_contract)
safe_actions = [
ProjectOperatingSuggestedAction(
action_key=f"prepare_next_action_{index}",
display_label=action,
safety="prepare_only",
reason="来自 PMFlow OS 门禁/执行态的只读建议,执行前仍需要用户确认边界。",
target_ref=f"project:{project_id}",
)
for index, action in enumerate(next_actions[:5], start=1)
]
blocked_actions = [
ProjectOperatingSuggestedAction(
action_key=f"blocked_by_evidence_{index}",
display_label=blocker,
safety="blocked",
reason="当前证据显示该阻塞未解除。",
target_ref=f"project:{project_id}",
)
for index, blocker in enumerate(blockers[:5], start=1)
]
return ProjectOperatingEvidencePacket(
project_id=project_id,
project_ref=f"project:{project_id}",
project_title=project_title,
project_status=project_status,
object_binding=_dict(object_binding),
command_state=command_state,
workbench_state=workbench_state,
decision_review_state=decision_review_state,
execution_state=execution_state,
quality_test_state=quality_test_state,
release_growth_state=release_growth_state,
team_state=team_state,
project_memory_state=project_memory_state,
risk_blocker_state=risk_blocker_state,
recent_timeline_state=recent_timeline_state,
evidence_refs=[ref],
safe_next_actions=safe_actions,
blocked_actions=blocked_actions,
)
def build_project_execution_findings(
*,
project_id: int,
title: str,
status: str,
truth_payload: dict[str, Any] | None,
execution_payload: dict[str, Any] | None,
fallback_task_counts: dict[str, Any] | None,
relevance: str,
) -> tuple[list[PMFlowAIEvidenceFinding], list[str]]:
"""Build bounded findings and coverage gaps for one project."""
truth = _dict(truth_payload)
execution_summary = _dict(execution_payload)
project = _dict(truth.get("project"))
execution = _dict(execution_summary.get("execution"))
acceptance = _dict(execution_summary.get("acceptance"))
release_gate = _dict(execution_summary.get("releaseGate"))
quality_contract = _dict(execution_summary.get("qualityContract"))
quality_summary = _dict(quality_contract.get("summary"))
project_title = _text(project.get("name") or project.get("title") or title or f"项目 {project_id}", 160)
project_status = _text(project.get("status") or status, 80)
project_stage = _text(project.get("stage") or truth.get("stage"), 120)
loop_stage = _text(truth.get("loopStage"), 120)
findings: list[PMFlowAIEvidenceFinding] = []
gaps: list[str] = []
overview_parts = [project_title]
if project_stage:
overview_parts.append(f"阶段:{project_stage}")
if project_status:
overview_parts.append(f"状态:{project_status}")
if loop_stage:
overview_parts.append(f"运行环节:{loop_stage}")
findings.append(
_finding(
fact="项目概况:" + ",".join(overview_parts) + "。",
implication="用于判断当前推进建议应绑定项目阶段和运行环节,而不是只看项目是否存在。",
relevance=relevance,
)
)
if not project_stage:
gaps.append("project_stage_missing")
current_task = _current_task(execution=execution, execution_summary=execution_summary)
if current_task:
task_title = _text(current_task.get("title"), 220)
task_status = _text(current_task.get("status"), 80)
execution_status = _text(execution.get("status"), 80)
task_id = _text(current_task.get("id") or execution.get("taskId"), 80)
parts = [task_title or f"任务 {task_id}" if task_id else "当前任务"]
if task_id:
parts.append(f"任务ID:{task_id}")
if task_status:
parts.append(f"任务状态:{task_status}")
if execution_status:
parts.append(f"执行状态:{execution_status}")
findings.append(
_finding(
fact="当前执行任务:" + ",".join(parts) + "。",
implication="这是当前推进顺序的第一锚点,优先处理它的完成证据和阻塞。",
relevance=relevance,
)
)
else:
gaps.append("current_execution_state_missing")
acceptance_status = _text(acceptance.get("status"), 100)
required_action = _text(acceptance.get("requiredAction"), 160)
if acceptance_status or required_action:
parts = []
if acceptance_status:
parts.append(f"验收状态:{acceptance_status}")
if required_action:
parts.append(f"下一步要求:{required_action}")
findings.append(
_finding(
fact="执行验收:" + ",".join(parts) + "。",
implication="用于判断当前推进是否应先补验收材料、测试证据或人工确认。",
relevance=relevance,
)
)
gate_status = _text(release_gate.get("status"), 100)
gate_title = _text(release_gate.get("title"), 120)
gate_summary = _text(release_gate.get("summary"), 260)
if gate_status or gate_title or gate_summary:
parts = []
if gate_status:
parts.append(f"门禁状态:{gate_status}")
if gate_title:
parts.append(gate_title)
if gate_summary:
parts.append(gate_summary)
findings.append(
_finding(
fact="发布/验收门禁:" + ",".join(parts) + "。",
implication="门禁状态决定当前能否继续发布、交付或进入下一轮开发。",
relevance=relevance,
)
)
blockers = _project_blockers(acceptance=acceptance, release_gate=release_gate, quality_contract=quality_contract)
if blockers:
findings.append(
_finding(
fact="阻塞项:" + ";".join(blockers[:6]) + "。",
implication="推进建议应优先消除这些阻塞,而不是继续泛化规划。",
relevance=relevance,
)
)
else:
gaps.append("project_blockers_or_release_gate_missing")
active_tasks = _active_tasks(execution_summary)
if active_tasks:
task_summaries = []
for item in active_tasks[:8]:
task_title = _text(item.get("title"), 180)
if not task_title:
continue
task_status = _text(item.get("status"), 60)
task_summaries.append(f"{task_title}{f'({task_status})' if task_status else ''}")
if task_summaries:
findings.append(
_finding(
fact=f"候选任务:当前有 {len(active_tasks)} 个执行/待执行任务," + ";".join(task_summaries[:5]) + "。",
implication="这些任务构成可排序的推进队列,可用于给出下一步行动顺序。",
relevance=relevance,
)
)
else:
task_counts = _dict(fallback_task_counts)
count_summary = _task_count_summary(task_counts)
if count_summary:
findings.append(
_finding(
fact="候选任务:" + count_summary + "。",
implication="当前只拿到任务数量,缺少任务标题和阻塞细节。",
relevance=relevance,
confidence="medium",
)
)
else:
gaps.append("active_or_next_tasks_missing")
next_actions = _unique_texts(list(release_gate.get("nextActions") or []), limit=5)
if next_actions:
findings.append(
_finding(
fact="系统建议下一步:" + ";".join(next_actions[:4]) + "。",
implication="这是 PMFlow OS 根据门禁与证据链给出的直接推进动作。",
relevance=relevance,
)
)
memory_summary = _memory_summary(truth)
if memory_summary:
findings.append(
_finding(
fact="项目记忆:" + memory_summary,
implication="项目记忆用于区分已确认事实和仍待审的想法,避免重复规划。",
relevance=relevance,
confidence="medium",
)
)
quality_missing = _safe_int(quality_summary.get("releaseBlockingMissing"))
if quality_missing:
findings.append(
_finding(
fact=f"质量合同:还有 {quality_missing} 个发布阻塞级证据未完成。",
implication="当前推进应先补齐发布阻塞级证据,再讨论发布或扩大范围。",
relevance=relevance,
)
)
if not execution_summary:
gaps.append("project_execution_projection_missing")
return findings[:10], _unique_texts(gaps, limit=8)
def _facet(
*,
key: str,
label: str,
facts: list[str],
summary: str,
evidence_refs: list[ProjectOperatingEvidenceRef],
missing: list[str],
partial: bool = False,
) -> ProjectOperatingEvidenceFacet:
clean_facts = _unique_texts(facts, limit=12)
coverage = "covered" if clean_facts and not partial and not missing else "partial" if clean_facts else "missing"
return ProjectOperatingEvidenceFacet(
facet_key=key,
display_label=label,
coverage_status=coverage,
summary=_text(summary or ";".join(clean_facts[:4]), 500),
facts=clean_facts,
evidence_refs=evidence_refs,
remaining_gaps=[] if coverage == "covered" else missing,
)
def _read_command_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet:
facts = _unique_texts(
[
f"项目:{data.project_title}" if data.project_title else "",
f"阶段:{data.project_stage}" if data.project_stage else "",
f"状态:{data.project_status}" if data.project_status else "",
f"运行环节:{data.loop_stage}" if data.loop_stage else "",
f"最后写入:{_text(data.truth.get('lastWritebackAt'), 120)}" if data.truth.get("lastWritebackAt") else "",
],
limit=8,
)
return _facet(
key="command_state",
label="指挥台",
facts=facts,
summary=",".join(facts),
evidence_refs=[data.evidence_ref],
missing=["project_stage_missing"] if not data.project_stage else [],
)
def _read_workbench_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet:
task_facts = _task_facts(data)
requirements = _model_rows(data.truth.get("requirements"))
risks = _model_rows(data.truth.get("risks"))
facts = [
*task_facts,
f"需求 {len(requirements)} 个" if requirements else "",
f"风险 {len(risks)} 个" if risks else "",
]
return _facet(
key="workbench_state",
label="项目工作台",
facts=_unique_texts(facts, limit=12),
summary=";".join(_unique_texts(facts, limit=5)),
evidence_refs=[data.evidence_ref],
missing=[] if _unique_texts(facts, limit=12) else ["active_or_next_tasks_missing"],
partial=bool(task_facts and not data.active_tasks),
)
def _read_decision_review_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet:
reviewer_routing = _dict(data.truth.get("reviewerRouting"))
routing_summary = _dict(reviewer_routing.get("summary"))
change_sets = _list(data.truth.get("changeSets"))
change_items = _list(data.truth.get("changeItems"))
approvals = _list(data.truth.get("approvals"))
routing_items = _list(reviewer_routing.get("items"))
facts = _unique_texts(
[
f"待审变更集 {len(change_sets)} 个" if change_sets else "",
f"待审变更项 {len(change_items)} 个" if change_items else "",
f"审批记录 {len(approvals)} 条" if approvals else "",
f"待审路由 {len(routing_items)} 条" if routing_items else "",
f"待审数:{routing_summary.get('totalPendingReview')}" if routing_summary.get("totalPendingReview") is not None else "",
f"高风险待审:{routing_summary.get('highRiskCount')}" if routing_summary.get("highRiskCount") else "",
f"验收状态:{_text(data.acceptance.get('status'), 100)}" if data.acceptance.get("status") else "",
f"下一步要求:{_text(data.acceptance.get('requiredAction'), 160)}" if data.acceptance.get("requiredAction") else "",
],
limit=12,
)
return _facet(
key="decision_review_state",
label="决策与待确认",
facts=facts,
summary=";".join(facts[:5]),
evidence_refs=[data.evidence_ref],
missing=[] if facts else ["decision_review_state_missing"],
)
def _read_execution_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet:
prompt_package = _dict(data.execution_summary.get("promptPackage"))
repo_bindings = _list(data.execution_summary.get("repoBindings"))
recent_events = _list(data.execution_summary.get("recentProgressEvents"))
recent_deliveries = _list(data.execution_summary.get("recentDeliveries"))
facts = _unique_texts(
[
f"执行状态:{_text(data.execution.get('status'), 100)}" if data.execution.get("status") else "",
*_task_facts(data)[:4],
f"执行包:{_text(prompt_package.get('title') or prompt_package.get('id'), 160)}" if prompt_package else "",
f"代码仓库绑定 {len(repo_bindings)} 个" if repo_bindings else "",
f"进展事件 {len(recent_events)} 条" if recent_events else "",
f"交付记录 {len(recent_deliveries)} 条" if recent_deliveries else "",
],
limit=12,
)
return _facet(
key="execution_state",
label="开发执行",
facts=facts,
summary=";".join(facts[:5]),
evidence_refs=[data.evidence_ref],
missing=[] if data.current_task or data.execution.get("status") or recent_events else ["current_execution_state_missing"],
)
def _read_quality_test_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet:
test_runs = _model_rows(data.truth.get("testRuns"))
delivery_artifacts = _dict(data.execution_summary.get("deliveryArtifacts"))
artifact_review = _dict(data.execution_summary.get("artifactReview"))
artifact_completeness = _dict(data.execution_summary.get("artifactCompleteness"))
test_outputs = _list(delivery_artifacts.get("test_outputs") or delivery_artifacts.get("testOutputs"))
review_items = _list(artifact_review.get("items") or artifact_review.get("reviewItems"))
quality_blockers = _unique_texts(list(data.quality_contract.get("blockingReasons") or []), limit=6)
quality_missing = _safe_int(data.quality_summary.get("releaseBlockingMissing"))
facts = _unique_texts(
[
f"发布阻塞级证据未完成 {quality_missing} 个" if quality_missing else "",
f"测试记录 {len(test_runs)} 条" if test_runs else "",
f"测试输出 {len(test_outputs)} 条" if test_outputs else "",
f"交付审查项 {len(review_items)} 个" if review_items else "",
f"证据完整度:{_text(artifact_completeness.get('status'), 100)}" if artifact_completeness.get("status") else "",
*_quality_review_facts(review_items),
*quality_blockers,
],
limit=12,
)
return _facet(
key="quality_test_state",
label="测试与质量",
facts=facts,
summary=";".join(facts[:5]),
evidence_refs=[data.evidence_ref],
missing=[] if data.quality_contract or test_runs or test_outputs or artifact_review else ["quality_test_state_missing"],
partial=bool((data.quality_contract or artifact_review) and not facts),
)
def _read_release_growth_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet:
releases = _model_rows(data.truth.get("releases"))
metric_signals = _model_rows(data.truth.get("metricSignals"))
release_signals = _dict(data.execution_summary.get("releaseSignals"))
next_actions = _unique_texts(list(data.release_gate.get("nextActions") or []), limit=6)
feedback = _list(release_signals.get("feedback") or release_signals.get("feedbackIngests"))
recommendations = _list(release_signals.get("recommendations") or release_signals.get("iterationRecommendations"))
facts = _unique_texts(
[
f"门禁状态:{_text(data.release_gate.get('status'), 100)}" if data.release_gate.get("status") else "",
_text(data.release_gate.get("title"), 120),
_text(data.release_gate.get("summary"), 260),
f"发布记录 {len(releases)} 条" if releases else "",
f"增长信号 {len(metric_signals)} 条" if metric_signals else "",
f"反馈输入 {len(feedback)} 条" if feedback else "",
f"迭代建议 {len(recommendations)} 条" if recommendations else "",
*[f"建议动作:{item}" for item in next_actions],
],
limit=12,
)
return _facet(
key="release_growth_state",
label="发布与增长",
facts=facts,
summary=";".join(facts[:5]),
evidence_refs=[data.evidence_ref],
missing=[] if data.release_gate or releases or metric_signals or release_signals else ["release_growth_state_missing"],
partial=bool((data.release_gate or release_signals) and not facts),
)
def _read_team_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet:
reviewer_routing = _dict(data.truth.get("reviewerRouting"))
routing_summary = _dict(reviewer_routing.get("summary"))
routing_items = _list(reviewer_routing.get("items"))
team_inbox = _dict(data.truth.get("teamInbox"))
inbox_counts = [
f"{label} {len(rows)}"
for label, rows in (
("产品收件箱", _list(team_inbox.get("product"))),
("工程收件箱", _list(team_inbox.get("engineering"))),
("测试收件箱", _list(team_inbox.get("qa"))),
("增长收件箱", _list(team_inbox.get("growth"))),
)
if rows
]
facts = _unique_texts(
[
*_team_facts(project=data.project, execution=data.execution, active_tasks=data.active_tasks),
*inbox_counts,
f"待审路由 {len(routing_items)} 条" if routing_items else "",
f"缺审查人 {routing_summary.get('missingReviewerCount')} 项" if routing_summary.get("missingReviewerCount") else "",
*_reviewer_suggestion_facts(routing_items),
],
limit=12,
)
return _facet(
key="team_state",
label="团队层",
facts=facts,
summary=";".join(facts[:5]),
evidence_refs=[data.evidence_ref],
missing=[] if facts else ["team_state_missing"],
)
def _read_project_memory_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet:
memory = _dict(data.truth.get("projectMemory"))
source_summary = _dict(memory.get("sourceSummary"))
memory_summary = _memory_summary(data.truth)
facts = _unique_texts(
[
memory_summary.rstrip("。") if memory_summary else "",
*[f"{key} {value}" for key, value in source_summary.items() if value],
],
limit=12,
)
return _facet(
key="project_memory_state",
label="项目记忆",
facts=facts,
summary=";".join(facts[:5]),
evidence_refs=[data.evidence_ref],
missing=[] if facts else ["project_memory_missing"],
)
def _read_risk_blocker_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet:
risks = _model_rows(data.truth.get("risks"))
warnings = _unique_texts(list(data.truth.get("projectionWarnings") or []) + list(data.execution_summary.get("warnings") or []), limit=8)
blockers = _project_blockers(acceptance=data.acceptance, release_gate=data.release_gate, quality_contract=data.quality_contract)
artifact_review = _dict(data.execution_summary.get("artifactReview"))
review_blockers = _unique_texts(list(artifact_review.get("blockingReasons") or []), limit=6)
risk_facts = _unique_texts(
[
*blockers,
*review_blockers,
*[f"风险:{_text(item.get('title') or item.get('summary'), 180)}" for item in risks[:5] if isinstance(item, dict)],
*[f"投影警告:{item}" for item in warnings[:4]],
"已读取验收/门禁/质量信息,未发现明确阻塞项。" if not blockers and not review_blockers and (data.acceptance or data.release_gate or data.quality_contract) else "",
],
limit=12,
)
return _facet(
key="risk_blocker_state",
label="风险与阻塞",
facts=risk_facts,
summary=";".join(risk_facts[:5]),
evidence_refs=[data.evidence_ref],
missing=[] if risk_facts else ["project_blockers_or_release_gate_missing"],
partial=bool(risk_facts and not blockers and not review_blockers),
)
def _read_recent_timeline_state(data: ProjectOperatingEvidenceReaderInput) -> ProjectOperatingEvidenceFacet:
facts = _timeline_facts(execution_summary=data.execution_summary, current_task=data.current_task, truth=data.truth)
return _facet(
key="recent_timeline_state",
label="最近动态",
facts=facts,
summary=";".join(facts[:5]),
evidence_refs=[data.evidence_ref],
missing=[] if facts else ["recent_timeline_missing"],
partial=bool(not facts and (data.current_task or data.active_tasks)),
)
def _task_facts(data: ProjectOperatingEvidenceReaderInput) -> list[str]:
facts: list[str] = []
if data.current_task:
task_title = _text(data.current_task.get("title"), 220)
task_status = _text(data.current_task.get("status"), 80)
task_id = _text(data.current_task.get("id") or data.execution.get("taskId"), 80)
facts.append(
"当前任务:" + ",".join(
part
for part in [
task_title or (f"任务 {task_id}" if task_id else "当前任务"),
f"任务ID:{task_id}" if task_id else "",
f"状态:{task_status}" if task_status else "",
]
if part
)
)
if data.active_tasks:
facts.append(f"执行/待执行任务 {len(data.active_tasks)} 个")
for item in data.active_tasks[:5]:
task_title = _text(item.get("title"), 160)
task_status = _text(item.get("status"), 60)
if task_title:
facts.append(f"{task_title}{f'({task_status})' if task_status else ''}")
else:
count_summary = _task_count_summary(data.task_counts)
if count_summary:
facts.append(count_summary)
return _unique_texts(facts, limit=10)
def _project_blockers(
*,
acceptance: dict[str, Any],
release_gate: dict[str, Any],
quality_contract: dict[str, Any],
) -> list[str]:
return _unique_texts(
[
*list(acceptance.get("blockingReasons") or []),
*list(release_gate.get("blockingReasons") or []),
*list(quality_contract.get("blockingReasons") or []),
],
limit=8,
)
def _team_facts(
*,
project: dict[str, Any],
execution: dict[str, Any],
active_tasks: list[dict[str, Any]],
) -> list[str]:
candidates: list[Any] = [
project.get("ownerName"),
project.get("owner"),
project.get("responsibleUserName"),
execution.get("assigneeName"),
execution.get("assignee"),
]
for item in active_tasks[:5]:
candidates.extend([item.get("assigneeName"), item.get("ownerName"), item.get("reviewerName")])
facts = [f"负责人/协作者:{value}" for value in _unique_texts(candidates, limit=6)]
return facts
def _quality_review_facts(review_items: list[Any]) -> list[str]:
out: list[str] = []
for item in review_items[:6]:
if not isinstance(item, dict):
continue
title = _text(item.get("title") or item.get("item") or item.get("summary"), 160)
status = _text(item.get("status"), 80)
if title:
out.append(f"交付审查:{title}{f'({status})' if status else ''}")
return _unique_texts(out, limit=6)
def _reviewer_suggestion_facts(rows: list[Any]) -> list[str]:
out: list[str] = []
for item in rows[:6]:
if not isinstance(item, dict):
continue
reviewer = _dict(item.get("suggestedReviewer"))
display_name = _text(reviewer.get("displayName") or reviewer.get("role"), 120)
reason = _text(item.get("reviewReason") or reviewer.get("reason"), 180)
if display_name or reason:
out.append("审查建议:" + ",".join(part for part in [display_name, reason] if part))
return _unique_texts(out, limit=6)
def _timeline_facts(*, execution_summary: dict[str, Any], current_task: dict[str, Any], truth: dict[str, Any] | None = None) -> list[str]:
rows: list[Any] = []
truth_payload = _dict(truth)
rows.extend(_list(truth_payload.get("feed"))[:6])
for key in ("recentProgressEvents", "recentDeliveries", "recentEvents", "timeline", "activities", "progressEvents", "events"):
value = execution_summary.get(key)
if isinstance(value, list):
rows.extend(value[:6])
facts: list[str] = []
for item in rows[:8]:
if isinstance(item, dict):
text = _text(item.get("summary") or item.get("title") or item.get("description") or item.get("message"), 220)
status = _text(item.get("status"), 80)
if text:
facts.append(f"{text}{f'({status})' if status else ''}")
else:
text = _text(item, 220)
if text:
facts.append(text)
if not facts and current_task:
title = _text(current_task.get("title"), 160)
status = _text(current_task.get("status"), 80)
if title:
facts.append(f"当前最近可见动态来自正在处理的任务:{title}{f'({status})' if status else ''}")
return _unique_texts(facts, limit=8)
def _model_rows(value: Any) -> list[dict[str, Any]]:
rows = value if isinstance(value, list) else []
out: list[dict[str, Any]] = []
for item in rows:
if isinstance(item, dict):
out.append(dict(item))
elif hasattr(item, "model_dump"):
out.append(item.model_dump(mode="json"))
return out
def _list(value: Any) -> list[Any]:
return list(value) if isinstance(value, list) else []
def _finding(
*,
fact: str,
implication: str,
relevance: str,
confidence: str = "high",
) -> PMFlowAIEvidenceFinding:
return PMFlowAIEvidenceFinding(
fact=_text(fact, 700),
source="PMFlow 项目执行态证据",
implication=_text(implication, 700),
relevance_to_goal=_text(relevance, 160),
confidence=confidence if confidence in {"high", "medium", "low", "unknown"} else "unknown",
)
def _current_task(*, execution: dict[str, Any], execution_summary: dict[str, Any]) -> dict[str, Any]:
task_id = _text(execution.get("taskId"), 80)
active_tasks = _active_tasks(execution_summary)
if task_id:
for item in active_tasks:
if _text(item.get("id"), 80) == task_id:
return item
if active_tasks:
return active_tasks[0]
prompt = _text(execution.get("prompt"), 700)
title = _extract_prompt_line(prompt, "- Title:")
status = _extract_prompt_line(prompt, "- Status:")
if title or task_id:
return {"id": task_id, "title": title, "status": status}
return {}
def _active_tasks(execution_summary: dict[str, Any]) -> list[dict[str, Any]]:
rows = execution_summary.get("activeDevTasks")
if not isinstance(rows, list):
return []
return [dict(item) for item in rows if isinstance(item, dict)]
def _memory_summary(truth: dict[str, Any]) -> str:
memory = _dict(truth.get("projectMemory"))
truth_nodes = list(memory.get("truthNodeFacts") or [])
semantic_facts = list(memory.get("semanticFacts") or [])
rows: list[str] = []
for item in truth_nodes[:4]:
if isinstance(item, dict):
text = _text(item.get("summary") or item.get("title") or item.get("fact"), 160)
else:
text = _text(item, 160)
if text:
rows.append(text)
if not rows:
for item in semantic_facts[:4]:
if isinstance(item, dict):
text = _text(item.get("summary") or item.get("value") or item.get("fact"), 160)
else:
text = _text(item, 160)
if text:
rows.append(text)
return ";".join(_unique_texts(rows, limit=4)) + ("。" if rows else "")
def _task_count_summary(task_counts: dict[str, Any]) -> str:
parts: list[str] = []
for key, label in (
("todo", "待处理"),
("pending", "待处理"),
("in_progress", "进行中"),
("blocked", "阻塞"),
("done", "已完成"),
):
value = _safe_int(task_counts.get(key))
if value:
parts.append(f"{label} {value}")
return "任务:" + "、".join(parts[:5]) if parts else ""
def _extract_prompt_line(prompt: str, prefix: str) -> str:
for line in prompt.splitlines():
if line.strip().startswith(prefix):
return _text(line.split(prefix, 1)[-1].strip(), 220)
return ""
def _dict(value: Any) -> dict[str, Any]:
return dict(value) if isinstance(value, dict) else {}
def _text(value: Any, limit: int) -> str:
return str(value or "").strip().replace("\x00", "")[:limit]
def _safe_int(value: Any) -> int:
try:
if value is None or value == "":
return 0
return int(value)
except Exception:
return 0
def _unique_texts(values: list[Any], *, limit: int) -> list[str]:
out: list[str] = []
for value in values:
text = _text(value, 240)
if text and text not in out:
out.append(text)
if len(out) >= limit:
break
return out