PMFlow/backend/app/services/pmflow_ai_operating_runtime/run_service.py

2581 lines
114 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.

"""Operating run kernel for PMFlow AI multi-agent runtime."""
from __future__ import annotations
import json
import time
import re
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from ...models import User
from ...schemas.pmflow_ai_operating_runtime import (
PMFlowAIAgentAssignment,
PMFlowAIAgentWorkProduct,
PMFlowAIAutonomyDecision,
PMFlowAICustomerSafeClaim,
PMFlowAICustomerSafeClaimLedger,
PMFlowAIEvidenceLedgerEntry,
PMFlowAIOperatingCycleState,
PMFlowAIOperatingCommandPayload,
PMFlowAIOperatingMission,
PMFlowAIOperatingMissionCriticResult,
PMFlowAIOperatingPlan,
PMFlowAIOperatingRunEvent,
PMFlowAIOperatingRunPacket,
PMFlowAIProductObjectProjection,
PMFlowAIProjectWorldFacet,
PMFlowAIProjectWorldStatePacket,
new_operating_id,
utc_now_iso,
)
from ...schemas.rag import RAGAskRequest
from ..ai_function_runtime_profile_service import AIFunctionRuntimeProfileService
from ..llm_service import llm_service
from ..pmflow_ai_agent_runtime import PMFlowAIAgentRunService
from ..pmflow_ai_agent_runtime.agent_evidence_quality import (
is_usable_research_claim_text,
looks_like_source_noise,
normalize_evidence_text,
)
from .agent_registry import PMFlowAIOperatingAgentRegistry
from .autonomy import PMFlowAIAutonomyPolicyService
from .council import PMFlowAIAgentCouncilService
from .cycle import OperatingCycleService
from .mission_critic import PMFlowAIOperatingMissionCriticService, merge_missing_roles_into_mission
from .mission_service import PMFlowAIOperatingMissionService
from .project_world_state_service import PMFlowAIProjectWorldStateService
from .specialist_agents import PMFlowAISpecialistAgentService
from ..prompt_runtime_service import PromptRuntimeService
from ..runtime_audit_context import bind_agent_audit_context
PROMPT_KEY_PMFLOW_AI_OPERATING_ANSWER_COMPOSER = "PROMPT_PMFLOW_AI_OPERATING_ANSWER_COMPOSER"
FUNCTION_NAME_PMFLOW_AI_OPERATING_ANSWER_COMPOSER = "pmflow_ai_operating_answer_composer"
SCENE_KEY_PMFLOW_AI_OPERATING_RUNTIME = "space_pmflow_ai_operating_runtime"
class PMFlowAIOperatingRunService:
"""Create mission-first multi-agent operating runs."""
def __init__(
self,
db: AsyncSession,
*,
tenant_id: int,
mission_service: PMFlowAIOperatingMissionService | None = None,
agent_registry: PMFlowAIOperatingAgentRegistry | None = None,
project_world_state_service: PMFlowAIProjectWorldStateService | None = None,
specialist_agent_service: PMFlowAISpecialistAgentService | None = None,
mission_critic_service: PMFlowAIOperatingMissionCriticService | None = None,
council_service: PMFlowAIAgentCouncilService | None = None,
autonomy_policy_service: PMFlowAIAutonomyPolicyService | None = None,
operating_cycle_service: OperatingCycleService | None = None,
delivery_runtime: Any | None = None,
) -> None:
self.db = db
self.tenant_id = int(tenant_id)
self.agent_registry = agent_registry or PMFlowAIOperatingAgentRegistry()
self.mission_service = mission_service or PMFlowAIOperatingMissionService(
db=db,
tenant_id=self.tenant_id,
agent_registry=self.agent_registry,
)
self.project_world_state_service = project_world_state_service or PMFlowAIProjectWorldStateService(db, tenant_id=self.tenant_id)
self.specialist_agent_service = specialist_agent_service or PMFlowAISpecialistAgentService(
db=db,
tenant_id=self.tenant_id,
agent_registry=self.agent_registry,
)
self.mission_critic_service = mission_critic_service or PMFlowAIOperatingMissionCriticService(
db=db,
tenant_id=self.tenant_id,
agent_registry=self.agent_registry,
)
self.council_service = council_service or PMFlowAIAgentCouncilService()
self.autonomy_policy_service = autonomy_policy_service or PMFlowAIAutonomyPolicyService()
self.operating_cycle_service = operating_cycle_service or OperatingCycleService()
self.delivery_runtime = delivery_runtime or PMFlowAIAgentRunService(db, tenant_id=self.tenant_id)
async def _compose_operating_final_answer(
self,
*,
request: RAGAskRequest,
mission: PMFlowAIOperatingMission,
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger,
work_products: list[PMFlowAIAgentWorkProduct],
council_review: Any,
autonomy_gate: Any,
operating_plan: PMFlowAIOperatingPlan,
product_object_projections: list[PMFlowAIProductObjectProjection],
delivery_answer: str,
) -> str:
generate = getattr(llm_service, "generate", None)
if not callable(generate) or not callable(getattr(self.db, "execute", None)):
return ""
try:
action_context = {
"prepared_action_packets": list(operating_plan.prepared_action_packets or [])[:8],
"confirmation_requests": list(operating_plan.confirmation_requests or [])[:8],
"execution_preparations": list(operating_plan.execution_preparations or [])[:8],
"product_object_projections": [
item.model_dump(mode="json", exclude_none=True) for item in product_object_projections[:8]
],
}
operating_decision = {
"council_review": council_review.model_dump(mode="json", exclude_none=True)
if hasattr(council_review, "model_dump")
else dict(council_review or {}),
"autonomy_gate": autonomy_gate.model_dump(mode="json", exclude_none=True)
if hasattr(autonomy_gate, "model_dump")
else dict(autonomy_gate or {}),
}
prompt_result = await PromptRuntimeService(self.db).render(
tenant_id=self.tenant_id,
prompt_key=PROMPT_KEY_PMFLOW_AI_OPERATING_ANSWER_COMPOSER,
question=str(request.question or "").strip()[:1600],
mission_json=_json_for_prompt(mission.model_dump(mode="json", exclude_none=True), limit=7000),
claim_ledger_json=_json_for_prompt(
customer_safe_claim_ledger.model_dump(mode="json", exclude_none=True),
limit=7000,
),
work_products_json=_json_for_prompt(
[item.model_dump(mode="json", exclude_none=True) for item in work_products],
limit=14000,
),
operating_decision_json=_json_for_prompt(operating_decision, limit=7000),
action_context_json=_json_for_prompt(action_context, limit=8000),
delivery_answer=str(delivery_answer or "").strip()[:5000],
)
runtime_profile = await AIFunctionRuntimeProfileService(self.db).resolve_profile(
function_name=FUNCTION_NAME_PMFLOW_AI_OPERATING_ANSWER_COMPOSER,
scene_key=SCENE_KEY_PMFLOW_AI_OPERATING_RUNTIME,
prompt_key=PROMPT_KEY_PMFLOW_AI_OPERATING_ANSWER_COMPOSER,
)
with bind_agent_audit_context(agent_step_key="operating_answer_composer"):
raw = await generate(
prompt_result.prompt,
db=self.db,
tenant_id=self.tenant_id,
function_name=FUNCTION_NAME_PMFLOW_AI_OPERATING_ANSWER_COMPOSER,
use_prompt_profile=False,
temperature=runtime_profile.temperature,
response_mode=runtime_profile.response_mode,
max_tokens=runtime_profile.max_tokens,
routing_meta={
"scene_key": SCENE_KEY_PMFLOW_AI_OPERATING_RUNTIME,
"runtime": "operating_answer_runtime",
"scene_contract": "post_operating_final_answer",
**runtime_profile.routing_meta(),
**prompt_result.routing_meta(),
},
)
answer = _plain_customer_text(raw, limit=8000)
if not answer or _looks_like_internal_answer_text(answer):
return ""
return answer
except Exception:
return ""
async def run_turn(
self,
*,
request: RAGAskRequest,
current_user: User,
conversation_context: dict[str, Any] | None,
tenant_project_context: dict[str, Any] | None,
started_at: float | None = None,
event_sink: Any | None = None,
) -> dict[str, Any]:
started = started_at or time.perf_counter()
delivery_kwargs = {
"request": request,
"current_user": current_user,
"conversation_context": conversation_context,
"tenant_project_context": tenant_project_context,
"started_at": started,
}
if event_sink is not None:
delivery_kwargs["event_sink"] = event_sink
delivery_payload = await self.delivery_runtime.run_turn(**delivery_kwargs)
if _agent_os_delivery_can_stand_alone(delivery_payload):
return _standalone_agent_os_delivery_payload(
delivery_payload=delivery_payload,
started=started,
)
mission = await self.mission_service.build_mission(
request=request,
current_user=current_user,
conversation_context=conversation_context,
tenant_project_context=tenant_project_context,
)
initial_critic = await self.mission_critic_service.review(
user_question=str(request.question or ""),
mission=mission,
artifact_slots=[],
evidence_lanes=[],
current_object_binding=_current_object_binding_from_context(
conversation_context=conversation_context,
tenant_project_context=tenant_project_context,
),
)
mission = merge_missing_roles_into_mission(mission=mission, critic=initial_critic)
project_world_state = await self.project_world_state_service.build_packet(
mission=mission,
current_user=current_user,
tenant_project_context=tenant_project_context,
)
assignments = self.agent_registry.assignments_for_mission(mission)
events = [
PMFlowAIOperatingRunEvent(
event_type="mission_created",
summary="已建立本轮 Operating Mission,先定义要推进的目标和确认边界。",
),
PMFlowAIOperatingRunEvent(
event_type="agents_assigned",
summary=f"已分派 {len(assignments)} 个专业 agent 产出结构化工作结果。",
),
PMFlowAIOperatingRunEvent(
event_type="project_world_state_read",
summary=_project_world_state_event_summary(project_world_state),
),
]
if _delivery_is_terminal_boundary(delivery_payload):
return _boundary_delivery_payload(
delivery_payload=delivery_payload,
mission=mission,
started=started,
)
customer_safe_claim_ledger = _build_customer_safe_claim_ledger(delivery_payload, mission=mission)
project_world_state = _merge_delivery_research_evidence(
project_world_state,
delivery_payload,
customer_safe_claim_ledger=customer_safe_claim_ledger,
)
if _delivery_can_finish_without_deep_operating_chain(
delivery_payload=delivery_payload,
mission=mission,
project_world_state=project_world_state,
customer_safe_claim_ledger=customer_safe_claim_ledger,
):
return _shallow_operating_delivery_payload(
delivery_payload=delivery_payload,
mission=mission,
project_world_state=project_world_state,
customer_safe_claim_ledger=customer_safe_claim_ledger,
started=started,
)
post_evidence_critic = await self.mission_critic_service.review(
user_question=str(request.question or ""),
mission=mission,
artifact_slots=_artifact_slots_from_delivery(delivery_payload),
evidence_lanes=_evidence_lanes_from_world_state(
project_world_state=project_world_state,
delivery_payload=delivery_payload,
customer_safe_claim_ledger=customer_safe_claim_ledger,
),
current_object_binding=_current_object_binding_from_delivery(
delivery_payload=delivery_payload,
conversation_context=conversation_context,
tenant_project_context=tenant_project_context,
),
)
mission = merge_missing_roles_into_mission(mission=mission, critic=post_evidence_critic)
assignments = self.agent_registry.assignments_for_mission(mission)
async_producer = getattr(self.specialist_agent_service, "produce_work_products_async", None)
if callable(async_producer):
work_products = await async_producer(
mission=mission,
assignments=assignments,
project_world_state=project_world_state,
learning_ledger={},
)
else:
work_products = self.specialist_agent_service.produce_work_products(
mission=mission,
assignments=assignments,
project_world_state=project_world_state,
)
council_review, operating_plan = self.council_service.review_and_compile(
mission=mission,
project_world_state=project_world_state,
work_products=work_products,
)
autonomy_gate, operating_plan = self.autonomy_policy_service.apply(
mission=mission,
project_world_state=project_world_state,
work_products=work_products,
council_review=council_review,
operating_plan=operating_plan,
)
operating_cycle = self.operating_cycle_service.build_cycle_state(
request=request,
mission=mission,
project_world_state=project_world_state,
council_review=council_review,
autonomy_gate=autonomy_gate,
operating_plan=operating_plan,
conversation_context=conversation_context,
)
operating_plan = _attach_cycle_to_plan(operating_plan=operating_plan, operating_cycle=operating_cycle)
product_object_projections = [
PMFlowAIProductObjectProjection.model_validate(item)
for item in list(operating_plan.product_object_projections or [])
if isinstance(item, dict)
]
command_payload = _build_command_payload(
mission=mission,
assignments=assignments,
project_world_state=project_world_state,
work_products=work_products,
council_review=council_review,
autonomy_gate=autonomy_gate,
operating_cycle=operating_cycle,
operating_plan=operating_plan,
product_object_projections=product_object_projections,
delivery_payload=delivery_payload,
customer_safe_claim_ledger=customer_safe_claim_ledger,
)
status = _operating_status(operating_plan)
specialist_answer = _compose_specialist_answer(
mission=mission,
project_world_state=project_world_state,
work_products=work_products,
council_review=council_review,
autonomy_gate=autonomy_gate,
operating_cycle=operating_cycle,
product_object_projections=product_object_projections,
operating_plan=operating_plan,
)
operating_run = PMFlowAIOperatingRunPacket(
run_id=new_operating_id("orun"),
mission=mission,
status=status,
assignments=assignments,
project_world_state=project_world_state,
customer_safe_claim_ledger=customer_safe_claim_ledger,
work_products=work_products,
agent_council_review=council_review,
autonomy_gate=autonomy_gate,
operating_cycle=operating_cycle,
product_object_projections=product_object_projections,
operating_plan=operating_plan,
events=[
*events,
*_mission_critic_events(
initial_critic=initial_critic,
post_evidence_critic=post_evidence_critic,
),
*[
PMFlowAIOperatingRunEvent(
event_type="specialist_work_product_ready",
summary=f"{product.product_type} 已生成。",
agent_id=product.agent_id,
assignment_id=product.assignment_id,
)
for product in work_products
],
PMFlowAIOperatingRunEvent(
event_type="agent_council_review_ready",
summary=council_review.resolution_summary,
),
PMFlowAIOperatingRunEvent(
event_type="operating_plan_compiled",
summary="已把 Agent Council 会审结果编译进 Operating Plan;回答交付只作为客户侧摘要适配。",
),
PMFlowAIOperatingRunEvent(
event_type="autonomy_gate_applied",
summary=_autonomy_gate_event_summary(autonomy_gate),
),
PMFlowAIOperatingRunEvent(
event_type="learning_ledger_updated",
summary=_learning_event_summary(operating_cycle),
),
],
delivery_ref={
"adapter": "answer_delivery",
"agent_run_id": _agent_run_id(delivery_payload),
"answer_mode": str(delivery_payload.get("answer_mode") or ""),
},
completed_at=utc_now_iso(),
)
delivery_answer = _delivery_customer_answer(delivery_payload)
if _answer_conflicts_with_evidence(
delivery_answer,
project_world_state=project_world_state,
delivery_payload=delivery_payload,
customer_safe_claim_ledger=customer_safe_claim_ledger,
):
delivery_answer = ""
if _answer_conflicts_with_evidence(
specialist_answer,
project_world_state=project_world_state,
delivery_payload=delivery_payload,
customer_safe_claim_ledger=customer_safe_claim_ledger,
):
specialist_answer = ""
operating_answer = await self._compose_operating_final_answer(
request=request,
mission=mission,
customer_safe_claim_ledger=customer_safe_claim_ledger,
work_products=work_products,
council_review=council_review,
autonomy_gate=autonomy_gate,
operating_plan=operating_plan,
product_object_projections=product_object_projections,
delivery_answer=delivery_answer,
) if not _should_keep_delivery_open_reasoning_answer(
delivery_payload=delivery_payload,
project_world_state=project_world_state,
) else ""
final_answer = (
operating_answer
or _compose_answer_from_terminal_and_artifacts(
delivery_payload=delivery_payload,
specialist_answer=specialist_answer,
project_world_state=project_world_state,
customer_safe_claim_ledger=customer_safe_claim_ledger,
)
or specialist_answer
or delivery_answer
)
payload = dict(delivery_payload or {})
payload["answer"] = final_answer
payload["reply_text"] = final_answer
if isinstance(payload.get("answer_canvas"), dict):
payload["answer_canvas"] = {
**dict(payload["answer_canvas"]),
"direct_answer": final_answer,
"headline": "PMFlow AI",
}
else:
payload["answer_canvas"] = {
"direct_answer": final_answer,
"headline": "PMFlow AI",
"next_actions": [],
"action_candidates": [],
}
payload["answer_canvas"] = _merge_operating_canvas(
payload["answer_canvas"],
mission=mission,
assignments=assignments,
work_products=work_products,
operating_plan=operating_plan,
product_object_projections=product_object_projections,
)
payload["processing_stage"] = "multi_agent_operating_runtime"
payload["answer_mode"] = "operating_runtime"
payload["policy_version"] = "pmflow-multi-agent-operating-runtime-v1"
payload["operating_summary"] = specialist_answer
payload["operating_mission"] = mission.model_dump(mode="json", exclude_none=True)
payload["operating_mission_critic"] = post_evidence_critic.model_dump(mode="json", exclude_none=True)
payload["operating_mission_critic_reviews"] = [
initial_critic.model_dump(mode="json", exclude_none=True),
post_evidence_critic.model_dump(mode="json", exclude_none=True),
]
payload["project_world_state_packet"] = project_world_state.model_dump(mode="json", exclude_none=True)
payload["customer_safe_claim_ledger"] = customer_safe_claim_ledger.model_dump(mode="json", exclude_none=True)
payload["evidence_ledger"] = [item.model_dump(mode="json", exclude_none=True) for item in project_world_state.evidence_ledger]
payload["agent_assignments"] = [item.model_dump(mode="json", exclude_none=True) for item in assignments]
payload["agent_work_products"] = [item.model_dump(mode="json", exclude_none=True) for item in work_products]
payload["agent_council_review"] = council_review.model_dump(mode="json", exclude_none=True)
payload["conflict_ledger"] = [item.model_dump(mode="json", exclude_none=True) for item in council_review.conflict_ledger]
payload["autonomy_gate"] = autonomy_gate.model_dump(mode="json", exclude_none=True)
payload["prepared_action_packets"] = list(operating_plan.prepared_action_packets or [])
payload["confirmation_requests"] = list(operating_plan.confirmation_requests or [])
payload["execution_preparations"] = list(operating_plan.execution_preparations or [])
payload["product_object_projections"] = [item.model_dump(mode="json", exclude_none=True) for item in product_object_projections]
payload["operating_cycle"] = operating_cycle.model_dump(mode="json", exclude_none=True)
payload["consumed_learning_ledger"] = operating_cycle.consumed_learning_ledger.model_dump(mode="json", exclude_none=True)
payload["outcome_observations"] = [item.model_dump(mode="json", exclude_none=True) for item in operating_cycle.outcome_observations]
payload["learning_ledger"] = operating_cycle.learning_ledger.model_dump(mode="json", exclude_none=True)
payload["learning_updates"] = list(operating_plan.learning_updates or [])
payload["next_cycle_triggers"] = list(operating_plan.next_cycle_triggers or [])
payload["operating_plan"] = operating_plan.model_dump(mode="json", exclude_none=True)
payload["operating_run_packet"] = operating_run.model_dump(mode="json", exclude_none=True)
payload["command_payload"] = command_payload.model_dump(mode="json", exclude_none=True)
payload["answer_process_packet"] = _merge_operating_process(
payload.get("answer_process_packet"),
mission=mission,
assignments=assignments,
work_products=work_products,
project_world_state=project_world_state,
council_review=council_review.model_dump(mode="json", exclude_none=True),
autonomy_gate=autonomy_gate.model_dump(mode="json", exclude_none=True),
operating_cycle=operating_cycle.model_dump(mode="json", exclude_none=True),
initial_critic=initial_critic,
post_evidence_critic=post_evidence_critic,
customer_safe_claim_ledger=customer_safe_claim_ledger,
total_duration_ms=int((time.perf_counter() - started) * 1000),
)
return payload
def _merge_delivery_research_evidence(
project_world_state: PMFlowAIProjectWorldStatePacket,
delivery_payload: dict[str, Any],
*,
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger,
) -> PMFlowAIProjectWorldStatePacket:
"""Promote inner read-loop evidence into the operating world state."""
deliverable = delivery_payload.get("deliverable_packet") if isinstance(delivery_payload.get("deliverable_packet"), dict) else {}
workspace = delivery_payload.get("agent_workspace") if isinstance(delivery_payload.get("agent_workspace"), dict) else {}
observations = _delivery_observations(delivery_payload, workspace=workspace)
source_rows = _external_source_rows(observations)
research_lanes = _external_research_lanes(observations)
claims = [item.claim_text for item in list(customer_safe_claim_ledger.claims or []) if item.claim_text]
gaps = _gap_texts(workspace, deliverable, list(project_world_state.remaining_gaps or []))
if not source_rows and not claims and not gaps:
return project_world_state.model_copy(update={"customer_safe_claim_ledger": customer_safe_claim_ledger})
source_count = len(source_rows)
claim_count = len(claims)
status = "ready" if claim_count >= 3 and source_count >= 2 else "partial"
summary = f"已完成外部研究读取:{source_count} 个候选来源、{claim_count} 条可用判断、{len(gaps)} 个剩余证据缺口。"
facet = PMFlowAIProjectWorldFacet(
facet_key="external_research_evidence",
display_label="外部研究证据",
status=status,
summary=summary,
facts=[
{"fact_key": "source_candidates", "value": source_rows[:8]},
{"fact_key": "usable_claims", "value": claims[:8]},
{"fact_key": "evidence_gaps", "value": gaps[:8]},
{"fact_key": "research_lanes", "value": research_lanes[:8]},
],
evidence_refs=_dedupe([str(item.get("observation_id") or item.get("id") or "") for item in observations])[:12]
or ["agent_research_delivery"],
gaps=gaps[:12],
)
coverage = dict(project_world_state.coverage or {})
covered = _dedupe([*list(coverage.get("covered_facets") or []), facet.facet_key])
missing = [item for item in list(coverage.get("missing_facets") or []) if item != facet.facet_key]
remaining = [gap for gap in gaps if gap and not _looks_like_internal_token(str(gap))]
if not remaining and status != "ready":
remaining = ["还缺少跨来源验证的公开材料"]
return project_world_state.model_copy(
update={
"status": "ready" if project_world_state.status == "partial" and status == "ready" else project_world_state.status,
"facets": [*[item for item in project_world_state.facets if item.facet_key != facet.facet_key], facet],
"customer_safe_claim_ledger": customer_safe_claim_ledger,
"evidence_ledger": [
*list(project_world_state.evidence_ledger or []),
PMFlowAIEvidenceLedgerEntry(
project_ref=project_world_state.primary_project_ref or "space:external_research",
facet_key=facet.facet_key,
source_ref="agent_research_delivery",
summary=summary,
confidence="high" if status == "ready" else "medium",
),
],
"coverage": {
**coverage,
"covered_facets": covered,
"missing_facets": missing,
"coverage_score": max(float(coverage.get("coverage_score") or 0.0), 0.45 if status == "partial" else 0.65),
},
"remaining_gaps": remaining[:24],
}
)
def _agent_os_delivery_can_stand_alone(delivery_payload: dict[str, Any]) -> bool:
answer = _delivery_customer_answer(delivery_payload)
if not answer:
return False
if isinstance(delivery_payload.get("interaction_request"), dict) and delivery_payload.get("interaction_request"):
return False
packet = delivery_payload.get("agent_run_packet") if isinstance(delivery_payload.get("agent_run_packet"), dict) else {}
stop_reason = str(packet.get("stop_reason") or "").strip()
if stop_reason not in {"planner_ready_to_answer", "ready_to_answer", "completed"}:
return False
workspace = delivery_payload.get("agent_workspace") if isinstance(delivery_payload.get("agent_workspace"), dict) else {}
goal_frame = workspace.get("goal_frame") if isinstance(workspace.get("goal_frame"), dict) else {}
if str(goal_frame.get("source_policy") or "").strip() == "open_reasoning":
return True
tool_steps = list(packet.get("steps") or [])
if any(str(item.get("tool_name") or "").strip() in {"external_search", "inspect_source_detail"} for item in tool_steps if isinstance(item, dict)):
return True
return False
def _standalone_agent_os_delivery_payload(
*,
delivery_payload: dict[str, Any],
started: float,
) -> dict[str, Any]:
payload = dict(delivery_payload or {})
answer = _delivery_customer_answer(payload)
payload["answer"] = answer
payload["reply_text"] = answer
payload["processing_stage"] = "pmflow_agent_runtime"
payload["answer_mode"] = "pmflow_agent_runtime"
payload["policy_version"] = str(payload.get("policy_version") or "pmflow-agent-os-v2")
payload["agent_assignments"] = []
payload["agent_work_products"] = []
payload["prepared_action_packets"] = []
payload["confirmation_requests"] = []
payload["execution_preparations"] = []
payload["product_object_projections"] = []
packet = payload.get("answer_process_packet") if isinstance(payload.get("answer_process_packet"), dict) else {}
packet["packet_version"] = "customer_answer_process.v1"
packet["visibility"] = "customer"
packet["total_duration_ms"] = max(0, int((time.perf_counter() - started) * 1000))
packet["wall_clock_duration_ms"] = packet["total_duration_ms"]
payload["answer_process_packet"] = packet
return payload
def _external_source_rows(observations: list[dict[str, Any]]) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for observation in observations:
source = str(observation.get("source_kind") or observation.get("source") or "").strip()
facts = _dicts(observation.get("facts"))
if source not in {"external_search", "inspect_source_detail"} and not any(str(item.get("type") or "") in {"external_evidence", "external_source_text"} for item in facts):
continue
for fact in facts:
fact_type = str(fact.get("type") or "").strip()
if fact_type not in {"external_evidence", "external_source_text"}:
continue
url = str(fact.get("url") or "").strip()
title = str(fact.get("title") or url or "公开来源").strip()
summary = str(fact.get("summary") or fact.get("text") or "").strip()
row_summary = _external_source_row_summary(summary)
if not url and not row_summary:
continue
if looks_like_source_noise(title):
continue
if not row_summary:
continue
rows.append({"title": title[:220], "url": url[:500], "summary": row_summary[:420], "source": source})
return _dedupe_dicts_local(rows, key="url")[:12]
def _external_research_lanes(observations: list[dict[str, Any]]) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for observation in observations:
for fact in _dicts(observation.get("facts")):
fact_type = str(fact.get("type") or "")
if fact_type == "external_search_query":
for lane in _dicts(fact.get("research_lanes")):
item = {
"lane": _plain_customer_text(lane.get("lane") or lane.get("name") or lane.get("title"), limit=160),
"purpose": _plain_customer_text(lane.get("purpose") or lane.get("goal"), limit=220),
"minimum_useful_signal": _plain_customer_text(
lane.get("minimum_useful_signal") or lane.get("success_criteria"),
limit=260,
),
}
if item["lane"]:
rows.append({key: value for key, value in item.items() if value})
continue
if fact_type != "external_search_hypotheses":
continue
for branch in _dicts(fact.get("branch_searches")):
item = {
"lane": _plain_customer_text(branch.get("meaning") or branch.get("purpose"), limit=160),
"purpose": _plain_customer_text(branch.get("purpose"), limit=220),
"query": _plain_customer_text(branch.get("query"), limit=220),
}
if item["lane"]:
rows.append({key: value for key, value in item.items() if value})
return _dedupe_dicts_local(rows, key="lane")[:12]
def _external_source_row_summary(summary: Any) -> str:
text = normalize_evidence_text(summary)
if not text:
return ""
if not looks_like_source_noise(text):
return text[:420]
fragments = re.split(r"(?<=[。!?.!?])\s+|[\n\r]+|;|;", text)
for fragment in [*fragments, *_research_signal_windows(text)]:
fragment = fragment.strip(" \t|-—::;;,,")
if len(fragment) < 36:
continue
if looks_like_source_noise(fragment):
continue
if is_usable_research_claim_text(fragment):
return fragment[:420]
return ""
def _research_signal_windows(text: str) -> list[str]:
value = re.sub(r"\s+", " ", str(text or "")).strip()
if not value:
return []
pattern = re.compile(
r"\b(?:19|20)\d{2}(?:\b|年)|\d[\d,]*(?:\.\d+)?\s*(?:%|万|亿|千|百|million|billion|trillion|k|m|bn)(?:\b|元|人|次|个|条|家)?|\$|€|£|¥|人民币|美元|营收|收入|成本|融资|投资|增长|下降|需求|供给|采用|参会|预算|赞助|转化|销售|reported|grew|declined|reached|increased|decreased|expected|shows|forecast|projected|valued|budget|sponsor|attendance|attendee|attendees|adoption|revenue",
flags=re.I,
)
rows: list[str] = []
for match in pattern.finditer(value):
fragment = value[max(0, match.start() - 220) : min(len(value), match.end() + 260)]
fragment = _trim_page_shell_edges(fragment=fragment, signal_text=match.group(0))
if len(fragment) > 520:
fragment = fragment[:520].rsplit(" ", 1)[0] or fragment[:520]
if 36 <= len(fragment) <= 520:
rows.append(fragment)
return _dedupe(rows)[:20]
def _trim_page_shell_edges(*, fragment: str, signal_text: str) -> str:
value = str(fragment or "").strip(" \t|-—::;;,,")
lowered = value.lower()
signal_index = lowered.find(str(signal_text or "").lower())
if signal_index < 0:
return value
markers = (
"subscribe",
"sign in",
"newsletter",
"share",
"privacy",
"terms",
"advertisement",
"contact us",
"read more",
"premium",
"site map",
"copyright",
"登录",
"注册",
"隐私政策",
"用户协议",
"广告",
"联系我们",
"订阅",
)
cut = 0
for marker in markers:
idx = lowered.rfind(marker, 0, signal_index)
if idx >= 0:
cut = max(cut, idx + len(marker))
if cut:
value = value[cut:].strip(" \t|-—::;;,,")
lowered = value.lower()
suffix_cut = len(value)
for marker in markers:
idx = lowered.find(marker, max(0, len(str(signal_text or "")) + 24))
if idx >= 0:
suffix_cut = min(suffix_cut, idx)
return value[:suffix_cut].strip(" \t|-—::;;,,")
def _delivery_observations(delivery_payload: dict[str, Any], *, workspace: dict[str, Any]) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
rows.extend(_dicts(delivery_payload.get("agent_os_observations")))
packet = delivery_payload.get("agent_run_packet") if isinstance(delivery_payload.get("agent_run_packet"), dict) else {}
rows.extend(_dicts(packet.get("observations")))
rows.extend(_dicts(workspace.get("observations")))
rows.extend(_dicts(workspace.get("observation_ledger")))
normalized: list[dict[str, Any]] = []
for row in rows:
item = dict(row)
if "source_kind" not in item and item.get("source"):
item["source_kind"] = item.get("source")
if "id" not in item and item.get("observation_id"):
item["id"] = item.get("observation_id")
normalized.append(item)
return _dedupe_dicts_local(normalized, key="id")[:24]
def _build_customer_safe_claim_ledger(
delivery_payload: dict[str, Any],
*,
mission: PMFlowAIOperatingMission,
) -> PMFlowAICustomerSafeClaimLedger:
deliverable = delivery_payload.get("deliverable_packet") if isinstance(delivery_payload.get("deliverable_packet"), dict) else {}
workspace = delivery_payload.get("agent_workspace") if isinstance(delivery_payload.get("agent_workspace"), dict) else {}
terminal_state = delivery_payload.get("terminal_answer_state") if isinstance(delivery_payload.get("terminal_answer_state"), dict) else {}
answer_runtime_packet = delivery_payload.get("answer_runtime_packet") if isinstance(delivery_payload.get("answer_runtime_packet"), dict) else {}
observations = _delivery_observations(delivery_payload, workspace=workspace)
terminal_status = str(terminal_state.get("terminal_status") or "").strip()
blocked_statuses = {"waiting_for_evidence", "blocked", "refused_by_boundary", "waiting_for_user"}
if terminal_status in blocked_statuses:
return PMFlowAICustomerSafeClaimLedger(
mission_id=mission.mission_id,
status="blocked",
blocked_reason=_claim_ledger_blocked_reason(terminal_status),
)
entries: list[PMFlowAICustomerSafeClaim] = []
for index, row in enumerate(_dicts(terminal_state.get("claims"))):
_append_customer_safe_claim(
entries,
row=row,
source_kind="terminal_claim",
source_label="本轮回答判断",
source_ref=f"terminal_claim:{index}",
)
for index, row in enumerate(_dicts(workspace.get("claim_ledger")) + _dicts(workspace.get("claims"))):
_append_customer_safe_claim(
entries,
row=row,
source_kind="workspace_claim",
source_label="工作区判断",
source_ref=f"workspace_claim:{index}",
)
section_index = 0
for section in _dicts(deliverable.get("sections")):
if str(section.get("section_kind") or "") not in {"claims", "findings", "summary"}:
continue
for item in _dicts(section.get("items")):
_append_customer_safe_claim(
entries,
row=item,
source_kind="deliverable_claim",
source_label="结构化交付判断",
source_ref=f"deliverable_claim:{section_index}",
)
section_index += 1
for key in ("claims", "claim_ledger", "customer_safe_claims"):
for index, row in enumerate(_dicts(answer_runtime_packet.get(key))):
_append_customer_safe_claim(
entries,
row=row,
source_kind="answer_claim",
source_label="答案质量门判断",
source_ref=f"answer_claim:{key}:{index}",
)
for observation in observations:
observation_ref = str(observation.get("observation_id") or observation.get("id") or "").strip()
for index, fact in enumerate(_dicts(observation.get("facts"))):
fact_type = str(fact.get("type") or "").strip()
if fact_type not in {"normalized_claim", "claim", "finding"}:
continue
_append_customer_safe_claim(
entries,
row=fact,
source_kind="observation_claim",
source_label="已归一化观察判断",
source_ref=observation_ref or f"observation_claim:{index}",
)
deduped = _dedupe_customer_safe_claims(entries)
source_counts: dict[str, int] = {}
source_refs: list[dict[str, Any]] = []
evidence_refs: list[str] = []
for claim in deduped:
source_counts[claim.source_kind] = source_counts.get(claim.source_kind, 0) + 1
if claim.source_ref:
source_refs.append({"source_kind": claim.source_kind, "source_label": claim.source_label, "source_ref": claim.source_ref})
evidence_refs.extend(list(claim.evidence_refs or []))
status = "ready" if len(deduped) >= 3 else ("partial" if deduped else "empty")
return PMFlowAICustomerSafeClaimLedger(
mission_id=mission.mission_id,
status=status,
claims=deduped[:16],
claim_count=len(deduped[:16]),
source_counts=source_counts,
source_refs=_dedupe_dicts_local(source_refs, key="source_ref")[:24],
evidence_refs=_dedupe(evidence_refs)[:30],
)
def _should_keep_delivery_open_reasoning_answer(
*,
delivery_payload: dict[str, Any],
project_world_state: PMFlowAIProjectWorldStatePacket,
) -> bool:
if project_world_state.project_refs:
return False
observations = _delivery_observations(
delivery_payload,
workspace=delivery_payload.get("agent_workspace") if isinstance(delivery_payload.get("agent_workspace"), dict) else {},
)
if _external_source_rows(observations):
return False
terminal_state = delivery_payload.get("terminal_answer_state") if isinstance(delivery_payload.get("terminal_answer_state"), dict) else {}
claims = _dicts(terminal_state.get("claims"))
workspace = delivery_payload.get("agent_workspace") if isinstance(delivery_payload.get("agent_workspace"), dict) else {}
claims.extend(_dicts(workspace.get("claim_ledger")))
return any(str(item.get("claim_type") or "").strip() == "controlled_hypothesis_basis" for item in claims)
def _delivery_can_finish_without_deep_operating_chain(
*,
delivery_payload: dict[str, Any],
mission: PMFlowAIOperatingMission,
project_world_state: PMFlowAIProjectWorldStatePacket,
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger,
) -> bool:
if project_world_state.project_refs or list(getattr(mission.project_scope, "object_refs", []) or []):
return False
answer = _delivery_customer_answer(delivery_payload)
if not answer:
return False
if _answer_conflicts_with_evidence(
answer,
project_world_state=project_world_state,
delivery_payload=delivery_payload,
customer_safe_claim_ledger=customer_safe_claim_ledger,
):
return False
if isinstance(delivery_payload.get("interaction_request"), dict) and delivery_payload.get("interaction_request"):
return False
if str(delivery_payload.get("grounding_status") or "") in {"not_grounded", "insufficient_evidence"}:
return False
workspace = delivery_payload.get("agent_workspace") if isinstance(delivery_payload.get("agent_workspace"), dict) else {}
goal_frame = workspace.get("goal_frame") if isinstance(workspace.get("goal_frame"), dict) else {}
answer_policy = str(goal_frame.get("answer_policy") or "").strip()
missing_policy = str(goal_frame.get("missing_evidence_policy") or "").strip()
latency_policy = goal_frame.get("latency_policy") if isinstance(goal_frame.get("latency_policy"), dict) else {}
latency_mode = str(latency_policy.get("mode") or "").strip()
if answer_policy not in {"", "directional_answer", "partial_answer"}:
return False
if missing_policy not in {"", "answer_with_boundaries"}:
return False
if latency_mode not in {"", "fast", "standard"}:
return False
observations = _delivery_observations(delivery_payload, workspace=workspace)
has_external_sources = bool(_external_source_rows(observations))
has_open_reasoning_basis = _payload_has_controlled_hypothesis_basis(delivery_payload)
terminal = delivery_payload.get("terminal_answer_state") if isinstance(delivery_payload.get("terminal_answer_state"), dict) else {}
terminal_status = str(terminal.get("terminal_status") or "").strip()
agent_run_packet = delivery_payload.get("agent_run_packet") if isinstance(delivery_payload.get("agent_run_packet"), dict) else {}
stop_reason = str(agent_run_packet.get("stop_reason") or "").strip()
verifier_passed = any(
str(item.get("result") or "").strip() == "passed"
for item in _dicts(agent_run_packet.get("verifications"))
)
has_claims = bool(customer_safe_claim_ledger.claims)
runtime_ready = stop_reason in {"planner_ready_to_answer", "ready_to_answer", "completed"} or verifier_passed
terminal_ready = terminal_status in {"", "answerable", "partially_answerable", "answered", "completed"}
if not terminal_status and not runtime_ready and not has_open_reasoning_basis:
return False
return terminal_ready and (has_external_sources or has_open_reasoning_basis or has_claims)
def _payload_has_controlled_hypothesis_basis(delivery_payload: dict[str, Any]) -> bool:
terminal_state = delivery_payload.get("terminal_answer_state") if isinstance(delivery_payload.get("terminal_answer_state"), dict) else {}
claims = _dicts(terminal_state.get("claims"))
workspace = delivery_payload.get("agent_workspace") if isinstance(delivery_payload.get("agent_workspace"), dict) else {}
claims.extend(_dicts(workspace.get("claim_ledger")))
return any(str(item.get("claim_type") or "").strip() == "controlled_hypothesis_basis" for item in claims)
def _append_customer_safe_claim(
entries: list[PMFlowAICustomerSafeClaim],
*,
row: dict[str, Any],
source_kind: str,
source_label: str,
source_ref: str,
) -> None:
if row.get("allowed_to_state") is False:
return
text = str(row.get("claim_text") or row.get("summary") or row.get("text") or "").strip()
if not _is_customer_safe_claim_text(text):
return
entries.append(
PMFlowAICustomerSafeClaim(
claim_text=text,
source_kind=source_kind,
source_label=source_label,
source_ref=source_ref,
confidence=_normalize_claim_confidence(row.get("confidence")),
evidence_refs=_claim_evidence_refs(row),
object_refs=_claim_object_refs(row),
allowed_to_state=True,
)
)
def _dedupe_customer_safe_claims(entries: list[PMFlowAICustomerSafeClaim]) -> list[PMFlowAICustomerSafeClaim]:
merged: dict[str, PMFlowAICustomerSafeClaim] = {}
for claim in entries:
key = normalize_evidence_text(claim.claim_text)
if not key:
continue
existing = merged.get(key)
if existing is None:
merged[key] = claim
continue
merged[key] = existing.model_copy(
update={
"evidence_refs": _dedupe([*list(existing.evidence_refs or []), *list(claim.evidence_refs or [])])[:16],
"object_refs": _dedupe([*list(existing.object_refs or []), *list(claim.object_refs or [])])[:16],
"source_ref": existing.source_ref or claim.source_ref,
"source_kind": existing.source_kind or claim.source_kind,
"source_label": existing.source_label or claim.source_label,
"confidence": _highest_confidence(existing.confidence, claim.confidence),
}
)
return list(merged.values())
def _claim_evidence_refs(row: dict[str, Any]) -> list[str]:
values = row.get("evidence_refs") or row.get("source_refs") or row.get("sources") or []
if not isinstance(values, list):
values = [values]
out: list[str] = []
for value in values[:16]:
if isinstance(value, dict):
text = str(value.get("evidence_id") or value.get("source_ref") or value.get("url") or value.get("title") or "").strip()
else:
text = str(value or "").strip()
if text and text not in out:
out.append(text[:180])
return out
def _claim_object_refs(row: dict[str, Any]) -> list[str]:
values = row.get("object_refs") or row.get("target_refs") or row.get("project_refs") or []
if not isinstance(values, list):
values = [values]
return _dedupe([str(item or "").strip() for item in values if str(item or "").strip()])[:16]
def _normalize_claim_confidence(value: Any) -> str:
text = str(value or "").strip().lower()
if text in {"high", "medium", "low"}:
return text
return "unknown"
def _highest_confidence(left: str, right: str) -> str:
order = {"unknown": 0, "low": 1, "medium": 2, "high": 3}
return left if order.get(left, 0) >= order.get(right, 0) else right
def _claim_ledger_blocked_reason(terminal_status: str) -> str:
mapping = {
"waiting_for_evidence": "本轮仍在等待证据,不能把临时材料写成客户安全判断。",
"blocked": "本轮答案状态被阻断,不能生成客户安全判断。",
"refused_by_boundary": "本轮触发披露或权限边界,不能生成业务判断。",
"waiting_for_user": "本轮需要用户补充,不能生成客户安全判断。",
}
return mapping.get(terminal_status, "本轮未形成可客户展示的判断。")
def _claim_texts(
workspace: dict[str, Any],
deliverable: dict[str, Any],
*,
terminal_state: dict[str, Any] | None = None,
answer_runtime_packet: dict[str, Any] | None = None,
observations: list[dict[str, Any]] | None = None,
) -> list[str]:
terminal_status = str((terminal_state or {}).get("terminal_status") or "").strip()
if terminal_status in {"waiting_for_evidence", "blocked", "refused_by_boundary", "waiting_for_user"}:
return []
claims: list[str] = []
for row in _dicts((terminal_state or {}).get("claims")):
if row.get("allowed_to_state") is False:
continue
claims.append(str(row.get("claim_text") or row.get("summary") or row.get("text") or "").strip())
for row in _dicts(workspace.get("claim_ledger")) + _dicts(workspace.get("claims")):
if row.get("allowed_to_state") is False:
continue
claims.append(str(row.get("claim_text") or row.get("summary") or row.get("text") or "").strip())
for section in _dicts(deliverable.get("sections")):
if str(section.get("section_kind") or "") not in {"claims", "findings", "summary"}:
continue
for item in _dicts(section.get("items")):
if item.get("allowed_to_state") is False:
continue
claims.append(str(item.get("summary") or item.get("claim_text") or item.get("text") or "").strip())
for key in ("claims", "claim_ledger", "customer_safe_claims"):
for row in _dicts((answer_runtime_packet or {}).get(key)):
if row.get("allowed_to_state") is False:
continue
claims.append(str(row.get("claim_text") or row.get("summary") or row.get("text") or "").strip())
for observation in observations or []:
for fact in _dicts(observation.get("facts")):
if fact.get("allowed_to_state") is False:
continue
fact_type = str(fact.get("type") or "").strip()
if fact_type not in {"normalized_claim", "claim", "finding"}:
continue
claims.append(str(fact.get("claim_text") or fact.get("summary") or fact.get("text") or "").strip())
return _dedupe([item for item in claims if _is_customer_safe_claim_text(item)])[:12]
def _is_customer_safe_claim_text(text: Any) -> bool:
value = normalize_evidence_text(text)
if not value:
return False
if is_usable_research_claim_text(value):
return True
if len(value) < 18:
return False
if looks_like_source_noise(value) or _looks_like_internal_token(value):
return False
if re.search(r"#{2,}|---|^\s*[-*]\s+\*\*|\|\s*[-:]{2,}\s*\|", value):
return False
return True
def _gap_texts(workspace: dict[str, Any], deliverable: dict[str, Any], existing_gaps: list[str]) -> list[str]:
gaps = [str(item or "").strip() for item in existing_gaps if str(item or "").strip()]
for row in _dicts(workspace.get("gap_ledger")) + _dicts(workspace.get("remaining_gaps")):
gaps.append(str(row.get("summary") or row.get("gap_text") or row.get("text") or "").strip())
for section in _dicts(deliverable.get("sections")):
if str(section.get("section_kind") or "") not in {"gaps", "boundaries", "limitations"}:
continue
for item in _dicts(section.get("items")):
gaps.append(str(item.get("summary") or item.get("text") or "").strip())
return _dedupe([item for item in gaps if item])[:16]
def _artifact_slots_from_delivery(delivery_payload: dict[str, Any]) -> list[dict[str, Any]]:
canvas = delivery_payload.get("answer_canvas") if isinstance(delivery_payload.get("answer_canvas"), dict) else {}
artifacts = _dicts(canvas.get("artifacts"))
slots: list[dict[str, Any]] = []
for artifact in artifacts[:8]:
artifact_kind = str(artifact.get("artifact_kind") or artifact.get("kind") or "").strip()
artifact_title = str(artifact.get("title") or "").strip()
for slot in _dicts(artifact.get("slots"))[:12]:
slot_key = str(slot.get("slot_key") or slot.get("key") or "").strip()
summary = _plain_customer_text(slot.get("summary") or slot.get("display_goal") or slot.get("title"), limit=500)
if not slot_key and not summary:
continue
slots.append(
{
"artifact_kind": artifact_kind,
"artifact_title": artifact_title,
"slot_key": slot_key,
"display_goal": summary,
"must_cover": [
_plain_customer_text(item, limit=260)
for item in list(slot.get("must_cover") or [])
if _plain_customer_text(item, limit=260)
][:8],
}
)
deliverable = delivery_payload.get("deliverable_packet") if isinstance(delivery_payload.get("deliverable_packet"), dict) else {}
for section in _dicts(deliverable.get("sections"))[:8]:
kind = str(section.get("section_kind") or "").strip()
if not kind:
continue
summary = _plain_customer_text(section.get("title") or section.get("summary") or kind, limit=420)
slots.append({"artifact_kind": "deliverable_packet", "slot_key": kind, "display_goal": summary, "must_cover": []})
return _dedupe_dicts_local(slots, key="slot_key")[:24]
def _evidence_lanes_from_world_state(
*,
project_world_state: PMFlowAIProjectWorldStatePacket,
delivery_payload: dict[str, Any],
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger,
) -> list[dict[str, Any]]:
lanes: list[dict[str, Any]] = []
for facet in list(project_world_state.facets or [])[:12]:
facts = [
{"fact_key": fact.get("fact_key"), "count": len(fact.get("value") or []) if isinstance(fact.get("value"), list) else 1}
for fact in list(facet.facts or [])[:8]
if isinstance(fact, dict)
]
lanes.append(
{
"facet_key": facet.facet_key,
"display_label": facet.display_label,
"status": facet.status,
"summary": facet.summary,
"facts": facts,
}
)
ledger_claims = [
_plain_customer_text(item.claim_text, limit=300)
for item in list(customer_safe_claim_ledger.claims or [])
if item.claim_text
]
if ledger_claims:
lanes.append(
{
"lane": "customer_safe_claim_ledger",
"status": customer_safe_claim_ledger.status,
"summary": f"CustomerSafeClaimLedger 已形成 {len(ledger_claims)} 条客户安全判断。",
"claims": ledger_claims[:8],
}
)
return lanes[:24]
def _current_object_binding_from_context(
*,
conversation_context: dict[str, Any] | None,
tenant_project_context: dict[str, Any] | None,
) -> dict[str, Any]:
for container in (tenant_project_context, conversation_context):
if not isinstance(container, dict):
continue
surface_context = container.get("surface_context") if isinstance(container.get("surface_context"), dict) else {}
current = surface_context.get("current_focus_object") if isinstance(surface_context.get("current_focus_object"), dict) else {}
if current:
return dict(current)
visible = list(surface_context.get("visible_objects") or [])
if visible and isinstance(visible[0], dict):
return dict(visible[0])
return {}
def _current_object_binding_from_delivery(
*,
delivery_payload: dict[str, Any],
conversation_context: dict[str, Any] | None,
tenant_project_context: dict[str, Any] | None,
) -> dict[str, Any]:
packet = (
delivery_payload.get("customer_interaction_context_packet")
if isinstance(delivery_payload.get("customer_interaction_context_packet"), dict)
else {}
)
current = packet.get("current_focus_object") if isinstance(packet.get("current_focus_object"), dict) else {}
if current:
return dict(current)
return _current_object_binding_from_context(conversation_context=conversation_context, tenant_project_context=tenant_project_context)
def _mission_critic_events(
*,
initial_critic: PMFlowAIOperatingMissionCriticResult,
post_evidence_critic: PMFlowAIOperatingMissionCriticResult,
) -> list[PMFlowAIOperatingRunEvent]:
rows: list[PMFlowAIOperatingRunEvent] = []
for key, critic in (("mission_critic_initial", initial_critic), ("mission_critic_post_evidence", post_evidence_critic)):
if not critic.needs_replan and not critic.missing_capability_roles:
continue
rows.append(
PMFlowAIOperatingRunEvent(
event_type=key,
summary=critic.reason or f"OperatingMissionCritic 补齐 {len(critic.missing_capability_roles)} 个 specialist coverage 缺口。",
)
)
return rows
def _answer_conflicts_with_evidence(
answer: str,
*,
project_world_state: PMFlowAIProjectWorldStatePacket,
delivery_payload: dict[str, Any],
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger,
) -> bool:
text = str(answer or "")
if not text.strip():
return False
source_count, claim_count = _world_source_claim_counts(
project_world_state=project_world_state,
delivery_payload=delivery_payload,
customer_safe_claim_ledger=customer_safe_claim_ledger,
)
source_zero = bool(re.search(r"0\s*(?:个)?(?:候选)?来源", text))
claim_zero = bool(re.search(r"0\s*条(?:可用)?(?:判断|结论|证据|信息)", text))
if source_zero and source_count > 0:
return True
if claim_zero and claim_count > 0:
return True
return False
def _world_source_claim_counts(
*,
project_world_state: PMFlowAIProjectWorldStatePacket,
delivery_payload: dict[str, Any],
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger,
) -> tuple[int, int]:
source_count = 0
claim_count = len(list(customer_safe_claim_ledger.claims or []))
for facet in list(project_world_state.facets or []):
for fact in list(facet.facts or []):
if not isinstance(fact, dict):
continue
key = str(fact.get("fact_key") or "")
value = fact.get("value")
if key == "source_candidates" and isinstance(value, list):
source_count += len(value)
if key == "usable_claims" and isinstance(value, list):
claim_count += len(value)
return source_count, claim_count
def _compose_answer_from_terminal_and_artifacts(
*,
delivery_payload: dict[str, Any],
specialist_answer: str,
project_world_state: PMFlowAIProjectWorldStatePacket,
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger,
) -> str:
claims = [
_plain_customer_text(item.claim_text, limit=420)
for item in list(customer_safe_claim_ledger.claims or [])
if item.claim_text
]
claims = _dedupe([item for item in claims if item])[:4]
canvas = delivery_payload.get("answer_canvas") if isinstance(delivery_payload.get("answer_canvas"), dict) else {}
artifacts = _dicts(canvas.get("artifacts"))
blueprint = next(
(
item
for item in artifacts
if str(item.get("artifact_kind") or "").strip() == "product_blueprint"
or "蓝图" in str(item.get("title") or "")
or "blueprint" in str(item.get("artifact_kind") or "").lower()
),
{},
)
if not claims and not blueprint:
return ""
parts: list[str] = []
if claims:
parts.append("可以先给一个有边界的判断:" + ";".join(claims[:2]) + "。")
if blueprint:
slots = []
for slot in _dicts(blueprint.get("slots"))[:5]:
title = _plain_customer_text(slot.get("title") or slot.get("slot_key"), limit=120)
summary = _plain_customer_text(slot.get("summary") or slot.get("display_goal"), limit=260)
if title and summary:
slots.append(f"{title}:{summary}")
elif summary:
slots.append(summary)
if slots:
parts.append("我会把它收敛成产品蓝图来推进:" + ";".join(slots[:4]) + "。")
actions = _dicts(canvas.get("action_candidates"))
if actions:
labels = [_customer_action_label(item.get("title") or item.get("action_label")) for item in actions[:2]]
action_text = ";".join(item for item in labels if item)
if action_text:
parts.append(f"下一步可以准备待确认动作:{action_text}。")
gaps = _customer_research_gaps(list(project_world_state.remaining_gaps or []))
if gaps:
parts.append("证据边界是:" + ";".join(gaps[:2]) + "。")
if specialist_answer:
parts.append(_plain_customer_text(specialist_answer, limit=800))
return "".join(parts).strip()
def _dedupe(rows: list[str]) -> list[str]:
out: list[str] = []
for item in rows:
text = " ".join(str(item or "").split())[:700]
if text and text not in out:
out.append(text)
return out
def _dedupe_dicts_local(rows: list[dict[str, Any]], *, key: str) -> list[dict[str, Any]]:
seen: set[str] = set()
out: list[dict[str, Any]] = []
for row in rows:
value = str(row.get(key) or row.get("title") or row.get("summary") or "").strip()
if not value or value in seen:
continue
seen.add(value)
out.append(row)
return out
def _dicts(value: Any) -> list[dict[str, Any]]:
if not isinstance(value, list):
return []
return [item for item in value if isinstance(item, dict)]
def _json_for_prompt(value: Any, *, limit: int) -> str:
return json.dumps(value, ensure_ascii=False, default=str)[: max(500, int(limit))]
def _build_command_payload(
*,
mission: PMFlowAIOperatingMission,
assignments: list[PMFlowAIAgentAssignment],
project_world_state: PMFlowAIProjectWorldStatePacket,
work_products: list[PMFlowAIAgentWorkProduct],
council_review: Any,
autonomy_gate: PMFlowAIAutonomyDecision,
operating_cycle: PMFlowAIOperatingCycleState,
operating_plan: PMFlowAIOperatingPlan,
product_object_projections: list[PMFlowAIProductObjectProjection],
delivery_payload: dict[str, Any],
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger,
) -> PMFlowAIOperatingCommandPayload:
context_packet = (
delivery_payload.get("customer_interaction_context_packet")
if isinstance(delivery_payload.get("customer_interaction_context_packet"), dict)
else {}
)
return PMFlowAIOperatingCommandPayload(
mission={
"mission_id": mission.mission_id,
"goal": mission.mission_goal,
"desired_outcome": mission.desired_outcome,
"autonomy_level": mission.autonomy_level,
},
project_focus=context_packet.get("current_focus_object") if isinstance(context_packet.get("current_focus_object"), dict) else {},
project_world_state=_project_world_state_command_summary(project_world_state),
agent_council=[
{
"agent_id": assignment.agent_id,
"status": _product_for_agent(work_products=work_products, agent_id=assignment.agent_id).status
if _product_for_agent(work_products=work_products, agent_id=assignment.agent_id) is not None
else "blocked",
"work_product": assignment.expected_work_product,
"summary": _summary_for_agent(work_products=work_products, agent_id=assignment.agent_id),
}
for assignment in assignments
],
agent_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],
priority_order=list(council_review.priority_order or []),
risk_vetoes=list(council_review.risk_vetoes or []),
autonomy_gate=autonomy_gate.model_dump(mode="json", exclude_none=True),
operating_cycle=operating_cycle.model_dump(mode="json", exclude_none=True),
consumed_learning_ledger=operating_cycle.consumed_learning_ledger.model_dump(mode="json", exclude_none=True),
strategic_read=operating_plan.strategic_read,
operating_plan=operating_plan.model_dump(mode="json", exclude_none=True),
prepared_actions=list(operating_plan.prepared_action_packets or []),
confirmation_requests=list(operating_plan.confirmation_requests or []),
execution_preparations=list(operating_plan.execution_preparations or []),
product_object_projections=[item.model_dump(mode="json", exclude_none=True) for item in product_object_projections],
blocked_actions=list(operating_plan.blocked_actions or []),
evidence_boundary=list(operating_plan.evidence_boundary or []),
evidence_ledger=[item.model_dump(mode="json", exclude_none=True) for item in project_world_state.evidence_ledger[:20]],
customer_safe_claim_ledger=customer_safe_claim_ledger.model_dump(mode="json", exclude_none=True),
learning_updates=list(operating_plan.learning_updates or []),
next_cycle={
"status": "ready_for_next_operating_step",
"summary": "下一步应让专业 agent 基于当前项目世界状态和 learning ledger 原生产出更深入的经营计划。",
"triggers": list(operating_plan.next_cycle_triggers or []),
},
)
def _merge_operating_process(
process_packet: Any,
*,
mission: PMFlowAIOperatingMission,
assignments: list[PMFlowAIAgentAssignment],
work_products: list[PMFlowAIAgentWorkProduct],
project_world_state: PMFlowAIProjectWorldStatePacket,
council_review: dict[str, Any],
autonomy_gate: dict[str, Any],
operating_cycle: dict[str, Any],
initial_critic: PMFlowAIOperatingMissionCriticResult,
post_evidence_critic: PMFlowAIOperatingMissionCriticResult,
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger,
total_duration_ms: int,
) -> dict[str, Any]:
packet = dict(process_packet or {}) if isinstance(process_packet, dict) else {}
existing_steps = list(packet.get("steps") or []) if isinstance(packet.get("steps"), list) else []
packet["packet_version"] = "customer_answer_process.v1"
packet["visibility"] = "customer"
packet["answer_type"] = packet.get("answer_type") or "agent_runtime"
packet["route_display"] = packet.get("route_display") or "agent_runtime"
packet["steps"] = [
*_operating_process_steps(
mission=mission,
assignments=assignments,
work_products=work_products,
project_world_state=project_world_state,
council_review=council_review,
autonomy_gate=autonomy_gate,
operating_cycle=operating_cycle,
initial_critic=initial_critic,
post_evidence_critic=post_evidence_critic,
customer_safe_claim_ledger=customer_safe_claim_ledger,
),
*existing_steps,
]
packet["total_duration_ms"] = max(0, int(total_duration_ms or packet.get("total_duration_ms") or 0))
return packet
def _operating_process_steps(
*,
mission: PMFlowAIOperatingMission,
assignments: list[PMFlowAIAgentAssignment],
work_products: list[PMFlowAIAgentWorkProduct],
project_world_state: PMFlowAIProjectWorldStatePacket,
council_review: dict[str, Any],
autonomy_gate: dict[str, Any],
operating_cycle: dict[str, Any],
initial_critic: PMFlowAIOperatingMissionCriticResult,
post_evidence_critic: PMFlowAIOperatingMissionCriticResult,
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger,
) -> list[dict[str, Any]]:
critic_missing = _dedupe([*initial_critic.missing_capability_roles, *post_evidence_critic.missing_capability_roles])
return [
{
"key": "operating_mission",
"label": "确定 Operating Mission",
"step_type": "operating_mission",
"status": "completed",
"display_summary": mission.desired_business_outcome or mission.desired_outcome or mission.mission_goal,
},
{
"key": "operating_mission_critic",
"label": "校验 Mission 覆盖",
"step_type": "operating_mission_critic",
"status": "completed",
"display_summary": (
f"Mission Critic 根据工作产品槽位和证据通道补齐 {len(critic_missing)} 个专业能力。"
if critic_missing
else "Mission Critic 已确认当前专业能力覆盖足以支撑本轮交付。"
),
},
{
"key": "specialist_registry",
"label": "选择专业 Agent",
"step_type": "specialist_registry",
"status": "completed" if assignments else "blocked",
"display_summary": f"本轮根据 mission 和能力合同选择 {len(assignments)} 个专业 agent,产出 {len(work_products)} 个工作产品。",
},
{
"key": "project_world_state",
"label": "读取项目/空间状态",
"step_type": "project_world_state",
"status": "completed" if project_world_state.status in {"ready", "partial"} else "blocked",
"display_summary": _project_world_state_event_summary(project_world_state),
},
{
"key": "customer_safe_claim_ledger",
"label": "汇总客户安全判断",
"step_type": "customer_safe_claim_ledger",
"status": "completed" if customer_safe_claim_ledger.status in {"ready", "partial"} else customer_safe_claim_ledger.status,
"display_summary": _claim_ledger_process_summary(customer_safe_claim_ledger),
},
{
"key": "agent_council",
"label": "会审专业产物",
"step_type": "agent_council",
"status": "completed" if str(council_review.get("status") or "") != "blocked" else "blocked",
"display_summary": str(council_review.get("resolution_summary") or "")[:600],
},
{
"key": "autonomy_gate",
"label": "应用确认门禁",
"step_type": "autonomy_gate",
"status": "completed" if str(autonomy_gate.get("status") or "") != "blocked" else "blocked",
"display_summary": _autonomy_gate_process_summary(autonomy_gate),
},
{
"key": "learning_loop",
"label": "写入下一轮学习触发",
"step_type": "learning_loop",
"status": "completed",
"display_summary": _learning_process_summary(operating_cycle),
},
]
def _merge_operating_canvas(
canvas: dict[str, Any],
*,
mission: PMFlowAIOperatingMission,
assignments: list[PMFlowAIAgentAssignment],
work_products: list[PMFlowAIAgentWorkProduct],
operating_plan: PMFlowAIOperatingPlan,
product_object_projections: list[PMFlowAIProductObjectProjection],
) -> dict[str, Any]:
out = dict(canvas or {})
artifacts = [item for item in list(out.get("artifacts") or []) if isinstance(item, dict)]
artifacts.extend(
_operating_canvas_artifacts(
mission=mission,
assignments=assignments,
work_products=work_products,
operating_plan=operating_plan,
product_object_projections=product_object_projections,
)
)
out["artifacts"] = _dedupe_dicts_local(artifacts, key="title")[:6]
return out
def _operating_canvas_artifacts(
*,
mission: PMFlowAIOperatingMission,
assignments: list[PMFlowAIAgentAssignment],
work_products: list[PMFlowAIAgentWorkProduct],
operating_plan: PMFlowAIOperatingPlan,
product_object_projections: list[PMFlowAIProductObjectProjection],
) -> list[dict[str, Any]]:
slots = []
for product in work_products[:8]:
if product.status == "blocked" and not product.summary:
continue
slots.append(
{
"slot_key": product.agent_id,
"title": _customer_work_product_title(product),
"summary": _plain_customer_text(product.summary, limit=500),
"coverage_status": product.status,
}
)
for projection in product_object_projections[:4]:
slots.append(
{
"slot_key": projection.object_type,
"title": projection.title,
"summary": projection.summary,
"coverage_status": "prepared",
}
)
if not slots:
return []
return [
{
"artifact_id": f"operating_{mission.mission_id}",
"artifact_kind": "operating_mission",
"title": "本轮 Operating Mission 与专业产物",
"summary": operating_plan.strategic_read or mission.desired_business_outcome or mission.desired_outcome,
"slots": slots[:8],
"confidence": "medium",
"agent_count": len(assignments),
}
]
def _operating_status(operating_plan: PMFlowAIOperatingPlan) -> str:
if operating_plan.requires_confirmation:
return "waiting_for_confirmation"
return "completed"
def _agent_run_id(payload: dict[str, Any]) -> str:
packet = payload.get("agent_run_packet") if isinstance(payload.get("agent_run_packet"), dict) else {}
return str(packet.get("run_id") or "").strip()
def _delivery_customer_answer(delivery_payload: dict[str, Any]) -> str:
"""Prefer the already gated Agent Answer Runtime output as the customer answer.
The operating layer enriches the run with mission/work-product/command packets.
It must not replace a usable natural answer with its own process summary; that
summary is kept separately for audit and command surfaces.
"""
for key in ("answer", "reply_text"):
text = _plain_customer_text(delivery_payload.get(key), limit=5000)
if text and not _looks_like_internal_answer_text(text):
return text
canvas = delivery_payload.get("answer_canvas") if isinstance(delivery_payload.get("answer_canvas"), dict) else {}
text = _plain_customer_text(canvas.get("direct_answer"), limit=5000)
if text and not _looks_like_internal_answer_text(text):
return text
return ""
def _delivery_is_terminal_boundary(delivery_payload: dict[str, Any]) -> bool:
packet = delivery_payload.get("agent_run_packet") if isinstance(delivery_payload.get("agent_run_packet"), dict) else {}
terminal = delivery_payload.get("terminal_answer_state") if isinstance(delivery_payload.get("terminal_answer_state"), dict) else {}
stop_reason = str(packet.get("stop_reason") or "").strip()
terminal_status = str(terminal.get("terminal_status") or "").strip()
if stop_reason == "protected_internal_refusal":
return True
return terminal_status in {"refused_by_boundary"} and bool(_delivery_customer_answer(delivery_payload))
def _boundary_delivery_payload(
*,
delivery_payload: dict[str, Any],
mission: PMFlowAIOperatingMission,
started: float,
) -> dict[str, Any]:
payload = dict(delivery_payload or {})
answer = _delivery_customer_answer(payload)
payload["answer"] = answer
payload["reply_text"] = answer
payload["processing_stage"] = str(payload.get("processing_stage") or "agent_runtime")
payload["answer_mode"] = str(payload.get("answer_mode") or "agent_runtime")
payload["policy_version"] = "pmflow-multi-agent-operating-runtime-v1"
payload["operating_mission"] = mission.model_dump(mode="json", exclude_none=True)
payload["agent_assignments"] = []
payload["agent_work_products"] = []
payload["prepared_action_packets"] = []
payload["confirmation_requests"] = []
payload["execution_preparations"] = []
payload["product_object_projections"] = []
packet = payload.get("answer_process_packet") if isinstance(payload.get("answer_process_packet"), dict) else {}
packet["total_duration_ms"] = max(0, int((time.perf_counter() - started) * 1000))
payload["answer_process_packet"] = packet
return payload
def _shallow_operating_delivery_payload(
*,
delivery_payload: dict[str, Any],
mission: PMFlowAIOperatingMission,
project_world_state: PMFlowAIProjectWorldStatePacket,
customer_safe_claim_ledger: PMFlowAICustomerSafeClaimLedger,
started: float,
) -> dict[str, Any]:
payload = dict(delivery_payload or {})
answer = _delivery_customer_answer(payload)
payload["answer"] = answer
payload["reply_text"] = answer
if isinstance(payload.get("answer_canvas"), dict):
payload["answer_canvas"] = {
**dict(payload["answer_canvas"]),
"direct_answer": answer,
"headline": "PMFlow AI",
}
else:
payload["answer_canvas"] = {
"direct_answer": answer,
"headline": "PMFlow AI",
"next_actions": [],
"action_candidates": [],
}
payload["processing_stage"] = "multi_agent_operating_runtime"
payload["answer_mode"] = "agent_runtime_operating_shallow"
payload["policy_version"] = "pmflow-multi-agent-operating-runtime-v1"
payload["operating_summary"] = answer
payload["operating_mission"] = mission.model_dump(mode="json", exclude_none=True)
payload["project_world_state_packet"] = project_world_state.model_dump(mode="json", exclude_none=True)
payload["customer_safe_claim_ledger"] = customer_safe_claim_ledger.model_dump(mode="json", exclude_none=True)
payload["agent_assignments"] = []
payload["agent_work_products"] = []
payload["prepared_action_packets"] = []
payload["confirmation_requests"] = []
payload["execution_preparations"] = []
payload["product_object_projections"] = []
packet = payload.get("answer_process_packet") if isinstance(payload.get("answer_process_packet"), dict) else {}
steps = list(packet.get("steps") or []) if isinstance(packet.get("steps"), list) else []
packet["packet_version"] = "customer_answer_process.v1"
packet["visibility"] = "customer"
packet["steps"] = [
{
"key": "operating_runtime_scope_gate",
"label": "判断是否需要深度编排",
"step_type": "operating_scope_gate",
"status": "completed",
"display_summary": "当前材料已足够先给出带边界的方向判断,本轮不再追加更重的专业产物链。",
"duration_ms": 0,
"blocking": False,
},
*steps,
]
packet["total_duration_ms"] = max(0, int((time.perf_counter() - started) * 1000))
packet["wall_clock_duration_ms"] = packet["total_duration_ms"]
payload["answer_process_packet"] = packet
return payload
def _looks_like_internal_answer_text(text: str) -> bool:
value = str(text or "").strip()
if not value:
return True
lowered = value.lower()
internal_markers = (
"agent os",
"turnstate",
"goalframe",
"researchnote",
"claim_ledger",
"gap_ledger",
"answer_runtime_packet",
"runtime",
"trace",
"prompt",
)
if any(marker in lowered for marker in internal_markers):
return True
return _looks_like_internal_token(value)
def _attach_cycle_to_plan(
*,
operating_plan: PMFlowAIOperatingPlan,
operating_cycle: PMFlowAIOperatingCycleState,
) -> PMFlowAIOperatingPlan:
learning_updates = [
{
"learning_key": item.get("learning_key"),
"summary": item.get("summary"),
"outcome_type": item.get("outcome_type"),
"outcome_status": item.get("outcome_status"),
}
for item in list(operating_cycle.learning_ledger.learning_items or [])[-12:]
if isinstance(item, dict)
]
return operating_plan.model_copy(
update={
"operating_cycle": operating_cycle.model_dump(mode="json", exclude_none=True),
"consumed_learning_ledger": operating_cycle.consumed_learning_ledger.model_dump(mode="json", exclude_none=True),
"learning_updates": learning_updates,
"next_cycle_triggers": list(operating_cycle.next_cycle_triggers or []),
}
)
def _autonomy_gate_event_summary(autonomy_gate: PMFlowAIAutonomyDecision) -> str:
return (
f"Autonomy Gate 已生成 {len(autonomy_gate.prepared_action_packets)} 个准备动作包、"
f"{len(autonomy_gate.confirmation_requests)} 个确认请求和 "
f"{len(autonomy_gate.execution_preparations)} 个执行准备包;写入和 handoff 均未执行。"
)
def _autonomy_gate_process_summary(autonomy_gate: dict[str, Any]) -> str:
prepared_count = len(list(autonomy_gate.get("prepared_action_packets") or []))
confirmation_count = len(list(autonomy_gate.get("confirmation_requests") or []))
execution_count = len(list(autonomy_gate.get("execution_preparations") or []))
return (
f"已准备 {prepared_count} 个动作包、{confirmation_count} 个确认请求、"
f"{execution_count} 个执行交接包;写入/执行/发布/外部动作全部需要确认后再进入专用链路。"
)
def _learning_event_summary(operating_cycle: PMFlowAIOperatingCycleState) -> str:
return (
f"已消费 {len(operating_cycle.consumed_learning_ledger.learning_items)} 条历史学习,"
f"记录 {len(operating_cycle.outcome_observations)} 条结果观察,"
f"准备 {len(operating_cycle.next_cycle_triggers)} 个下一轮触发合同。"
)
def _learning_process_summary(operating_cycle: dict[str, Any]) -> str:
consumed = operating_cycle.get("consumed_learning_ledger") if isinstance(operating_cycle.get("consumed_learning_ledger"), dict) else {}
learning = operating_cycle.get("learning_ledger") if isinstance(operating_cycle.get("learning_ledger"), dict) else {}
observations = list(operating_cycle.get("outcome_observations") or [])
triggers = list(operating_cycle.get("next_cycle_triggers") or [])
return (
f"已读取 {len(list(consumed.get('learning_items') or []))} 条历史学习,"
f"本轮记录 {len(observations)} 条结果观察,"
f"学习账本累计 {len(list(learning.get('learning_items') or []))} 条,"
f"下一轮可由 {len(triggers)} 类事件触发。"
)
def _claim_ledger_process_summary(ledger: PMFlowAICustomerSafeClaimLedger) -> str:
if ledger.status == "blocked":
return ledger.blocked_reason or "本轮未形成可进入回答和项目世界状态的客户安全判断。"
source_count = len(dict(ledger.source_counts or {}))
return f"已从 {source_count} 类客户安全来源汇总 {len(list(ledger.claims or []))} 条判断,供项目世界状态、答案仲裁和后续产品蓝图共用。"
def _project_world_state_command_summary(project_world_state: PMFlowAIProjectWorldStatePacket) -> dict[str, Any]:
return {
"packet_version": project_world_state.packet_version,
"world_state_id": project_world_state.world_state_id,
"status": project_world_state.status,
"primary_project_ref": project_world_state.primary_project_ref,
"project_refs": list(project_world_state.project_refs or []),
"coverage": dict(project_world_state.coverage or {}),
"facet_summaries": [
{
"facet_key": facet.facet_key,
"display_label": facet.display_label,
"status": facet.status,
"summary": facet.summary,
}
for facet in list(project_world_state.facets or [])[:12]
],
"remaining_gaps": list(project_world_state.remaining_gaps or [])[:12],
}
def _compose_specialist_answer(
*,
mission: PMFlowAIOperatingMission,
project_world_state: PMFlowAIProjectWorldStatePacket,
work_products: list[PMFlowAIAgentWorkProduct],
council_review: Any,
autonomy_gate: PMFlowAIAutonomyDecision,
operating_cycle: PMFlowAIOperatingCycleState,
product_object_projections: list[PMFlowAIProductObjectProjection],
operating_plan: PMFlowAIOperatingPlan,
) -> str:
strategist = _product_for_agent(work_products=work_products, agent_id="product_strategy_agent")
execution = _product_for_agent(work_products=work_products, agent_id="pm_breakdown_agent")
risk = _product_for_agent(work_products=work_products, agent_id="qa_risk_agent")
project_ref = _display_scope_ref(project_world_state.primary_project_ref or "")
if project_world_state.status not in {"ready", "partial"}:
gaps = "、".join(project_world_state.remaining_gaps[:3]) or "缺少可安全绑定的项目对象"
return f"我还不能给出完整推进判断:{project_ref} 的项目世界状态没有读完整,当前缺口是 {gaps}。这轮只能先保留本轮目标和对象候选,不能用空间概览或单段回答冒充项目计划。"
lead = operating_plan.highest_leverage_move or mission.desired_outcome
strategy_text = _product_summary(strategist)
execution_text = _product_summary(execution)
research_product = _product_for_type(work_products=work_products, product_type="ResearchNote")
if research_product is not None and (project_world_state.primary_project_ref == "space:external_research" or not project_world_state.project_refs):
return _compose_research_operating_answer(
mission=mission,
project_world_state=project_world_state,
research_product=research_product,
operating_plan=operating_plan,
autonomy_gate=autonomy_gate,
operating_cycle=operating_cycle,
)
risk_blocks = list(council_review.risk_vetoes or []) or (list(risk.blocked_actions or []) if risk is not None else [])
risk_text = ""
if risk_blocks:
first_block = risk_blocks[0]
risk_text = f" 不能直接做的是:{first_block.get('title')},原因是 {first_block.get('reason')}。"
prepared = list(operating_plan.prepared_action_packets or [])
prepared_text = ""
if prepared:
prepared_text = " 我已经准备好候选动作包:" + ";".join(str(item.get("title") or "") for item in prepared[:3] if item.get("title")) + ",这些都需要你确认后才会写入或执行。"
projection_text = ""
if product_object_projections:
projection = product_object_projections[0]
projection_text = f" 本轮已经形成一个待确认的{projection.object_type}:{projection.title}。"
confirmation_text = ""
if autonomy_gate.confirmation_requests:
confirmation_text = f" 本轮有 {len(autonomy_gate.confirmation_requests)} 个确认请求;确认前只展示候选,不会写入项目资料、改动任务或交给执行环境。"
learning_text = ""
if operating_cycle.consumed_learning_ledger.learning_items:
learning_text = f" 我也读取了上一轮 {len(operating_cycle.consumed_learning_ledger.learning_items)} 条历史约束,避免重复使用已失败的推进方式。"
council_text = ""
if council_review.conflict_ledger:
council_text = f" Agent Council 已记录 {len(council_review.conflict_ledger)} 个冲突/边界,并收敛成单一优先级。"
fallback_context = project_world_state.facets[0].summary if project_world_state.facets else ""
context = strategy_text or execution_text or fallback_context
return f"{project_ref} 的最高杠杆动作是:{lead}。{context}{council_text}{projection_text}{prepared_text}{confirmation_text}{learning_text}{risk_text}".strip()
def _compose_research_operating_answer(
*,
mission: PMFlowAIOperatingMission,
project_world_state: PMFlowAIProjectWorldStatePacket,
research_product: PMFlowAIAgentWorkProduct | None,
operating_plan: PMFlowAIOperatingPlan,
autonomy_gate: PMFlowAIAutonomyDecision,
operating_cycle: PMFlowAIOperatingCycleState,
) -> str:
output = research_product.structured_output if research_product is not None and isinstance(research_product.structured_output, dict) else {}
evidence_state = output.get("evidence_state") if isinstance(output.get("evidence_state"), dict) else {}
source_count = int(evidence_state.get("source_count") or 0)
claims = _gated_research_claims(output)
claim_count = len(claims)
ready = bool(evidence_state.get("ready_for_opportunity_brief")) and claim_count > 0
gaps = _customer_research_gaps(list(output.get("research_gaps") or project_world_state.remaining_gaps or []))
source_plan = [item for item in list(output.get("source_plan") or []) if isinstance(item, dict)]
lead = operating_plan.highest_leverage_move or "继续补齐研究证据"
if ready and claims:
answer = "按目前能核验的材料,可以先形成一个初步判断:" + ";".join(claims[:3])
if gaps:
answer += f"。仍需保留的边界是:{';'.join(gaps[:2])}。"
elif claims:
answer = (
"现在可以先给一个有边界的初判:"
+ ";".join(claims[:3])
+ f"。但还不能升级成机会简报或执行动作:当前是 {source_count} 个候选来源、{claim_count} 条可用判断,"
"还需要跨来源补证。"
)
if gaps:
answer += f" 还缺:{';'.join(gaps[:3])}。"
else:
answer = (
"现在还不能直接下市场结论。"
f"已整理 {source_count} 个候选来源和 {claim_count} 条可用判断,"
f"接下来最值得补的是:{lead}。"
)
if gaps:
answer += f"主要缺口:{';'.join(gaps[:3])}。"
if source_plan:
lanes = []
for item in source_plan[:3]:
lane = str(item.get("lane") or item.get("goal") or "").strip()
query = str(item.get("query") or "").strip()
label = lane or query
if label:
lanes.append(label)
if lanes:
answer += " 下一步只推进前置研究补证:" + ";".join(lanes) + "。"
if autonomy_gate.confirmation_requests:
executable = [
item
for item in list(autonomy_gate.confirmation_requests or [])
if isinstance(item, dict) and str(item.get("action_type") or "").strip()
]
if executable:
labels = [_customer_action_label(item.get("action_label") or item.get("title")) for item in executable[:3]]
action_text = ";".join(item for item in labels if item)
if action_text:
answer += " 可继续的动作是:" + action_text + "。"
answer += "确认前只展示候选,不会写入项目资料、改动任务或交给执行环境。"
if operating_cycle.consumed_learning_ledger.learning_items:
answer += f" 这轮沿用了上一轮 {len(operating_cycle.consumed_learning_ledger.learning_items)} 条历史约束,但只用于避免重复走失败路径,不把历史失败当成结论。"
return answer.strip()
def _gated_research_claims(output: dict[str, Any]) -> list[str]:
rows = list(output.get("market_claims") or output.get("claims") or [])
out: list[str] = []
for item in rows[:12]:
text = _plain_customer_text(item, limit=420)
if not text or not is_usable_research_claim_text(text):
continue
if text not in out:
out.append(text)
return out
def _customer_research_gaps(rows: list[Any]) -> list[str]:
out: list[str] = []
for item in rows[:12]:
text = _plain_customer_text(item, limit=260)
if not text:
continue
if _looks_like_internal_token(text):
text = "还缺少可直接支撑结论的公开来源材料"
if text not in out:
out.append(text)
return out
def _customer_action_label(value: object) -> str:
text = _plain_customer_text(value, limit=80)
if not text:
return ""
if _looks_like_internal_token(text):
return ""
latin_count = sum(1 for ch in text if "a" <= ch.lower() <= "z")
cjk_count = sum(1 for ch in text if "\u4e00" <= ch <= "\u9fff")
if latin_count > 16 and cjk_count == 0:
return ""
return text
def _plain_customer_text(value: object, *, limit: int) -> str:
text = str(value or "").replace("\r\n", "\n").replace("\r", "\n").strip()
if not text:
return ""
lines = [re.sub(r"[ \t]+", " ", line).strip() for line in text.split("\n")]
text = "\n".join(lines)
text = re.sub(r"\n{3,}", "\n\n", text).strip()
text = _repair_numbered_markdown_table_continuations(text)
text = _repair_fragmented_markdown_tables(text)
text = _remove_orphan_markdown_pipe_lines(text)
text = _repair_inline_bold_boundaries(text)
text = _repair_inline_markdown_tables(text)
text = _repair_raw_markdown_headings(text)
text = _repair_inline_markdown_lists(text)
text = _repair_inline_section_breaks(text)
text = text[:limit].strip()
return text.replace("Project Memory", "项目资料").replace("handoff", "执行交接")
def _repair_inline_markdown_tables(text: str) -> str:
value = str(text or "").strip()
if not value or "|" not in value:
return value
paragraphs = re.split(r"\n\s*\n+", value)
repaired: list[str] = []
for paragraph in paragraphs:
item = paragraph.strip()
if not item:
continue
if "|" in item and "---" not in item and item.count("|") >= 4:
recovered = _recover_malformed_pipe_rows(item)
if recovered:
repaired.append(recovered)
continue
if not _looks_like_inline_markdown_table(item):
repaired.append(item)
continue
first_pipe = item.find("|")
if first_pipe > 0:
lead = item[:first_pipe].strip()
if lead:
repaired.append(lead)
item = item[first_pipe:].strip()
reconstructed = _reconstruct_inline_markdown_table(item)
if reconstructed:
repaired.append(reconstructed)
continue
table = re.sub(r"\s*\|\|\s*", " |\n| ", item)
table = re.sub(r"\s+(\|\s*:?-{3,}:?\s*\|)", r"\n\1", table)
table = re.sub(r"(\|\s*:?-{3,}:?\s*\|[ \t]*(?:\|[ \t]*:?-{3,}:?[ \t]*)*)\s+\|", r"\1\n|", table)
table = re.sub(r"\|\s+(:?-{3,}:?)", r"|\1", table)
table = re.sub(r"(:?-{3,}:?)\s+\|", r"\1|", table)
repaired.append("\n".join(line.strip() for line in table.splitlines() if line.strip()))
return "\n\n".join(repaired).strip()
def _remove_orphan_markdown_pipe_lines(text: str) -> str:
value = str(text or "").strip()
if not value or "|" not in value:
return value
lines = [line for line in value.split("\n") if not re.fullmatch(r"[ \t]*\|+[ \t]*", line)]
return re.sub(r"\n{3,}", "\n\n", "\n".join(lines)).strip()
def _repair_inline_bold_boundaries(text: str) -> str:
value = str(text or "").strip()
if "**" not in value:
return value
def repl(match: re.Match[str]) -> str:
body = match.group(1).strip()
before = value[match.start() - 1] if match.start() > 0 else ""
after = value[match.end()] if match.end() < len(value) else ""
prefix = " " if before and not re.fullmatch(r"\s|[((\[【{《“]", before) else ""
suffix = " " if after and not re.fullmatch(r"\s|[),)\]】}。;:,、!?,.!?:》”]", after) else ""
return f"{prefix}**{body}**{suffix}"
value = re.sub(r"\*\*([^*\n]+?)\*\*", repl, value)
return re.sub(r"[ \t]{2,}", " ", value).strip()
def _repair_fragmented_markdown_tables(text: str) -> str:
value = str(text or "").strip()
if not value or "|" not in value or "---" not in value:
return value
lines = value.split("\n")
out: list[str] = []
index = 0
while index < len(lines):
current = lines[index].strip()
next_line = lines[index + 1].strip() if index + 1 < len(lines) else ""
if not _is_markdown_pipe_line(current) or not _is_markdown_table_separator_line(next_line):
out.append(lines[index])
index += 1
continue
first_pipe = current.find("|")
lead = current[:first_pipe].strip() if first_pipe > 0 else ""
header_line = current[first_pipe:].strip() if first_pipe > 0 else current
segment = [header_line, next_line]
cursor = index + 2
crossed_blank_line = False
while cursor < len(lines):
candidate = lines[cursor].strip()
if not candidate:
next_table_index = _next_non_empty_line_index(lines, cursor + 1)
if next_table_index >= 0 and _is_markdown_pipe_line(lines[next_table_index].strip()):
crossed_blank_line = True
cursor += 1
continue
break
if not _is_markdown_pipe_line(candidate):
break
segment.append(candidate)
cursor += 1
reconstructed = _reconstruct_fragmented_markdown_table(segment)
if reconstructed and (crossed_blank_line or reconstructed != "\n".join(segment)):
if lead:
out.append(lead)
out.append(reconstructed)
index = cursor
continue
out.append(lines[index])
index += 1
return "\n".join(out).strip()
def _repair_numbered_markdown_table_continuations(text: str) -> str:
value = str(text or "").strip()
if not value or "|" not in value or "---" not in value:
return value
lines = value.split("\n")
out: list[str] = []
index = 0
while index < len(lines):
current = lines[index].strip()
next_line = lines[index + 1].strip() if index + 1 < len(lines) else ""
if not _is_markdown_pipe_line(current) or not _is_markdown_table_separator_line(next_line):
out.append(lines[index])
index += 1
continue
header = _markdown_pipe_cells(current, preserve_internal_empty=False)
separators = _markdown_pipe_cells(next_line, preserve_internal_empty=False)
column_count = len(header)
if column_count < 2 or len(separators) != column_count:
out.append(lines[index])
index += 1
continue
segment: list[str] = []
cursor = index + 2
saw_bare_continuation = False
while cursor < len(lines):
candidate = lines[cursor].strip()
if not candidate:
next_table_index = _next_non_empty_line_index(lines, cursor + 1)
next_candidate = lines[next_table_index].strip() if next_table_index >= 0 else ""
if next_candidate and (
_is_markdown_pipe_line(next_candidate)
or next_candidate.startswith("|")
or _is_numbered_table_continuation_line(next_candidate)
or (saw_bare_continuation and not _looks_like_new_markdown_block(next_candidate))
):
segment.append("")
cursor += 1
continue
break
if _is_standalone_markdown_heading(candidate):
break
if _is_markdown_pipe_line(candidate) or (saw_bare_continuation and candidate.startswith("|")):
segment.append(candidate)
cursor += 1
continue
if _is_numbered_table_continuation_line(candidate) or (segment and "|" in candidate):
saw_bare_continuation = True
segment.append(candidate)
cursor += 1
continue
if saw_bare_continuation and segment and not _looks_like_new_markdown_block(candidate):
segment.append(candidate)
cursor += 1
continue
break
if not saw_bare_continuation or not segment:
out.append(lines[index])
index += 1
continue
repaired = _reconstruct_numbered_continuation_table(header, separators, segment)
if not repaired:
out.append(lines[index])
index += 1
continue
out.append(repaired)
index = cursor
return "\n".join(out).strip()
def _is_standalone_markdown_heading(line: str) -> bool:
value = str(line or "").strip()
return bool(re.fullmatch(r"#{1,6}\s+\S.*", value) or re.fullmatch(r"\*\*[^*\n]{2,80}\*\*", value))
def _looks_like_new_markdown_block(line: str) -> bool:
value = str(line or "").strip()
return _is_standalone_markdown_heading(value) or bool(re.fullmatch(r"[-*+]\s+\S.*", value))
def _is_numbered_table_continuation_line(line: str) -> bool:
value = str(line or "").strip()
return bool(re.match(r"^\|?\s*\d+[.)、]\s+\S", value) or re.match(r"^\|{2,}\s*\d+[.)、]\s+\S", value))
def _reconstruct_numbered_continuation_table(
header: list[str],
separators: list[str],
lines: list[str],
) -> str:
column_count = len(header)
normalized = "\n".join(lines)
normalized = normalized.replace("||", "\n| ")
normalized = re.sub(r"\n{2,}", "\n", normalized).strip()
chunks = [
chunk.strip()
for chunk in re.split(r"(?=(?:^|\n)\s*\|?\s*\d+[.)、]\s+\S)", normalized)
if chunk.strip()
]
rows: list[list[str]] = []
malformed = False
for chunk in chunks:
row = _row_from_numbered_table_chunk(chunk, column_count)
if not row:
malformed = True
continue
if not chunk.strip().startswith("|") or len(_markdown_pipe_cells(chunk, preserve_internal_empty=False)) != column_count:
malformed = True
rows.append(row)
if not malformed or len(rows) < 2:
return ""
return "\n".join(
[
"| " + " | ".join(header) + " |",
"| " + " | ".join(separators) + " |",
*["| " + " | ".join(row) + " |" for row in rows],
]
)
def _row_from_numbered_table_chunk(chunk: str, column_count: int) -> list[str] | None:
cleaned = re.sub(r"^\|+", "", str(chunk or ""))
cleaned = re.sub(r"\|+$", "", cleaned).strip()
cells = [_normalize_markdown_table_cell(cell) for cell in cleaned.split("|")]
cells = [cell for cell in cells if cell]
if len(cells) >= column_count:
return [*cells[: column_count - 1], ";".join(cells[column_count - 1 :])][:column_count]
if len(cells) == column_count - 1 and column_count >= 3:
split = _split_numbered_table_lead_cell(cells[0])
if split:
cells = [split[0], split[1], *cells[1:]]
if len(cells) == column_count:
return cells
return None
def _split_numbered_table_lead_cell(value: str) -> tuple[str, str] | None:
text = _normalize_markdown_table_cell(value)
match = re.match(r"^((?:\d+[.)、]\s*)?\*\*[^*]{2,60}\*\*)\s+(.+)$", text)
if match:
return match.group(1).strip(), match.group(2).strip()
match = re.match(r"^(\d+[.)、]\s+[^,。;::、|]{2,28}?)[ \t]+(.+)$", text)
if match:
return match.group(1).strip(), match.group(2).strip()
return None
def _next_non_empty_line_index(lines: list[str], start: int) -> int:
for index in range(start, len(lines)):
if lines[index].strip():
return index
return -1
def _is_markdown_pipe_line(line: str) -> bool:
value = str(line or "").strip()
return value.startswith("|") and value.count("|") >= 2
def _is_markdown_table_separator_line(line: str) -> bool:
cells = _markdown_pipe_cells(line, preserve_internal_empty=False)
return len(cells) >= 2 and all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells)
def _markdown_pipe_cells(line: str, *, preserve_internal_empty: bool) -> list[str]:
cells = [_normalize_markdown_table_cell(cell) for cell in str(line or "").split("|")]
if cells and cells[0] == "":
cells = cells[1:]
if cells and cells[-1] == "":
cells = cells[:-1]
if preserve_internal_empty:
return cells
return [cell for cell in cells if cell]
def _normalize_markdown_table_cell(value: str) -> str:
cell = re.sub(r"<br\s*/?>", ";", str(value or ""), flags=re.IGNORECASE)
cell = re.sub(r"\s+", " ", cell)
cell = re.sub(r";{2,}", ";", cell)
return cell.strip()
def _reconstruct_fragmented_markdown_table(lines: list[str]) -> str:
if len(lines) < 3:
return ""
if any(re.search(r"#{2,6}\s*\S", line) for line in lines[2:]):
return ""
header = _markdown_pipe_cells(lines[0], preserve_internal_empty=False)
separators = _markdown_pipe_cells(lines[1], preserve_internal_empty=False)
column_count = len(header)
if column_count < 2 or len(separators) != column_count:
return ""
if not all(re.fullmatch(r":?-{3,}:?", cell) for cell in separators):
return ""
rows: list[list[str]] = []
buffer: list[str] = []
for line in lines[2:]:
cells = _markdown_pipe_cells(line, preserve_internal_empty=True)
non_empty_cells = [cell for cell in cells if cell]
if not non_empty_cells:
continue
can_use_whole_row = (
not buffer
and len(cells) == column_count
and bool(cells[0])
and len(non_empty_cells) >= max(2, column_count - 1)
)
if can_use_whole_row:
rows.append(cells)
continue
buffer.extend(non_empty_cells)
while len(buffer) >= column_count:
rows.append(buffer[:column_count])
buffer = buffer[column_count:]
if not rows:
return ""
table = "\n".join(
[
"| " + " | ".join(header) + " |",
"| " + " | ".join(separators) + " |",
*["| " + " | ".join(row) + " |" for row in rows],
]
)
trailing = " ".join(buffer).strip()
return "\n\n".join(part for part in [table, trailing] if part).strip()
def _recover_malformed_pipe_rows(item: str) -> str:
first_pipe = item.find("|")
lead = item[:first_pipe].strip() if first_pipe > 0 else ""
body = item[first_pipe:] if first_pipe >= 0 else item
cells, trailing = _split_heading_from_pipe_cells(body)
rows = _rows_from_pipe_cells(cells)
if not rows:
return ""
table = "\n".join(
[
"| 优先级 | 项目 | 说明 |",
"| :--- | :--- | :--- |",
*[f"| {row['priority']} | {row['title']} | {row['summary']} |" for row in rows],
]
)
return "\n\n".join(part for part in [lead, table, trailing] if part).strip()
def _split_heading_from_pipe_cells(body: str) -> tuple[list[str], str]:
cells = [cell.strip() for cell in str(body or "").split("|")]
cells = [cell for cell in cells if cell]
heading_index = next((index for index, cell in enumerate(cells) if re.search(r"#{2,6}\s*\S", cell)), -1)
if heading_index < 0:
return cells, ""
heading = re.sub(r"^#{2,6}\s*", "", cells[heading_index]).strip()
return cells[:heading_index], _format_markdown_heading_body(heading) if heading else ""
def _rows_from_pipe_cells(cells: list[str]) -> list[dict[str, str]]:
rows: list[dict[str, str]] = []
priority_re = re.compile(r"^(?:P\d+|高|中|低|High|Medium|Low)$", re.IGNORECASE)
index = 0
while index + 2 < len(cells):
if priority_re.fullmatch(cells[index]):
rows.append({"priority": cells[index], "title": cells[index + 1], "summary": cells[index + 2]})
index += 3
continue
if priority_re.fullmatch(cells[index + 2]):
rows.append({"priority": cells[index + 2], "title": cells[index], "summary": cells[index + 1]})
index += 3
continue
break
return [row for row in rows if row["title"] and row["summary"]]
def _reconstruct_inline_markdown_table(item: str) -> str:
cells = [cell.strip() for cell in str(item or "").split("|")]
cells = [cell for cell in cells if cell]
sep_index = next((index for index, cell in enumerate(cells) if re.fullmatch(r":?-{3,}:?", cell)), -1)
if sep_index < 2:
return ""
column_count = sep_index
separators = cells[sep_index : sep_index + column_count]
if len(separators) < column_count or not all(re.fullmatch(r":?-{3,}:?", cell) for cell in separators):
return ""
header = cells[:column_count]
rest = cells[sep_index + column_count :]
rows = [rest[index : index + column_count] for index in range(0, len(rest), column_count)]
rows = [row for row in rows if len(row) == column_count and any(cell for cell in row)]
if not rows:
return ""
return "\n".join(
[
"| " + " | ".join(header) + " |",
"| " + " | ".join(separators) + " |",
*["| " + " | ".join(row) + " |" for row in rows],
]
)
def _looks_like_inline_markdown_table(text: str) -> bool:
item = str(text or "").strip()
if "\n" in item or item.count("|") < 8:
return False
return bool(re.search(r"\|\s*:?-{3,}:?\s*\|", item))
def _repair_inline_markdown_lists(text: str) -> str:
value = str(text or "").strip()
if not value:
return ""
value = re.sub(r"([::])\s+((?:[-*+–—]|\d+[.)、])\s+)", r"\1\n\n\2", value)
value = re.sub(r"([。!?;))])\s*((?:[-*+–—]|\d+[.)、])\s+)", r"\1\n\n\2", value)
value = re.sub(r"(?<!\n)\n((?:[-*+–—]|\d+[.)、])\s+)", r"\n\n\1", value)
value = re.sub(r"^([–—])\s+", "- ", value, flags=re.MULTILINE)
return re.sub(r"\n{3,}", "\n\n", value).strip()
def _repair_inline_section_breaks(text: str) -> str:
value = str(text or "").strip()
if not value:
return ""
value = re.sub(r"^(?![ \t]*#{2,6}\s)([^。!?\n|]{3,36}?)[ \t]+[—–-][ \t]+(?=\S{2,24}[::])", r"**\1**\n\n", value)
value = re.sub(r"([。!?])[ \t]+([^。!?\n|]{3,36}?)[ \t]+[—–-][ \t]+(?=\S{2,24}[::])", r"\1\n\n**\2**\n\n", value)
return re.sub(r"\n{3,}", "\n\n", value).strip()
def _repair_raw_markdown_headings(text: str) -> str:
value = str(text or "").replace(r"\#", "#")
value = re.sub(r"([^\n])\s+(#{2,6}\s+\S)", r"\1\n\n\2", value)
return re.sub(
r"^[ \t]*#{2,6}\s+(.+)$",
lambda match: _format_markdown_heading_body(match.group(1)),
value,
flags=re.MULTILINE,
).strip()
def _format_markdown_heading_body(value: str) -> str:
body = re.sub(r"^#{2,6}\s+", "", str(value or "").strip())
if not body:
return ""
separator = re.search(r"\s+[—–-]\s+", body)
if not separator:
return f"**{body}**"
title = body[: separator.start()].strip()
rest = body[separator.end() :].strip()
if not title:
return rest
if not rest:
return f"**{title}**"
return f"**{title}**\n\n{rest}"
def _looks_like_internal_token(text: str) -> bool:
lowered = text.lower().strip()
if re.search(r"\b[a-z]+(?:_[a-z0-9]+){1,}\b", lowered):
return True
if re.search(r"\b(step|run|packet|trace|prompt|function)_[a-z0-9_]+\b", lowered):
return True
return False
def _project_world_state_event_summary(project_world_state: PMFlowAIProjectWorldStatePacket) -> str:
coverage = project_world_state.coverage or {}
covered = len(list(coverage.get("covered_facets") or []))
partial = len(list(coverage.get("partial_facets") or []))
missing = len(list(coverage.get("missing_facets") or []))
if project_world_state.status == "missing_scope":
return "没有强项目对象,本轮不会用空间项目数量或 active 状态冒充项目世界状态。"
return f"已读取 {len(project_world_state.project_refs)} 个项目对象的世界状态;覆盖 {covered} 项、部分覆盖 {partial} 项、缺失 {missing} 项。"
def _display_scope_ref(value: str) -> str:
text = str(value or "").strip()
if text == "space:external_research":
return "这个机会研究"
if text.startswith("project:"):
return "当前项目"
if not text:
return "当前对象"
return text
def _summary_for_agent(*, work_products: list[PMFlowAIAgentWorkProduct], agent_id: str) -> str:
product = _product_for_agent(work_products=work_products, agent_id=agent_id)
return product.summary if product else ""
def _product_for_agent(*, work_products: list[PMFlowAIAgentWorkProduct], agent_id: str) -> PMFlowAIAgentWorkProduct | None:
return next((item for item in work_products if item.agent_id == agent_id), 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 _product_summary(product: PMFlowAIAgentWorkProduct | None) -> str:
return str(product.summary if product is not None else "").strip()[:500]
def _customer_work_product_title(product: PMFlowAIAgentWorkProduct) -> str:
output = product.structured_output if isinstance(product.structured_output, dict) else {}
for key in ("title", "artifact_title", "summary_title", "research_question", "idea_title"):
title = _plain_customer_text(output.get(key), limit=120)
if title:
return title
return _plain_customer_text(product.product_type, limit=120) or "专业工作产品"
__all__ = ["PMFlowAIOperatingRunService"]