"""Operating run kernel for PMFlow AI multi-agent runtime.""" from __future__ import annotations 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, PMFlowAIEvidenceLedgerEntry, PMFlowAIOperatingCycleState, PMFlowAIOperatingCommandPayload, PMFlowAIOperatingMission, PMFlowAIOperatingPlan, PMFlowAIOperatingRunEvent, PMFlowAIOperatingRunPacket, PMFlowAIProductObjectProjection, PMFlowAIProjectWorldFacet, PMFlowAIProjectWorldStatePacket, new_operating_id, utc_now_iso, ) from ...schemas.rag import RAGAskRequest 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_service import PMFlowAIOperatingMissionService from .project_world_state_service import PMFlowAIProjectWorldStateService from .specialist_agents import PMFlowAISpecialistAgentService 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, 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() 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 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() mission = await self.mission_service.build_mission( request=request, current_user=current_user, conversation_context=conversation_context, tenant_project_context=tenant_project_context, ) 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), ), ] 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) project_world_state = _merge_delivery_research_evidence(project_world_state, delivery_payload) 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, ) 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, 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, *[ 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(), ) payload = dict(delivery_payload or {}) payload["answer"] = specialist_answer payload["reply_text"] = specialist_answer if isinstance(payload.get("answer_canvas"), dict): payload["answer_canvas"] = { **dict(payload["answer_canvas"]), "direct_answer": specialist_answer, "headline": "PMFlow AI", } payload["processing_stage"] = "multi_agent_operating_runtime" payload["answer_mode"] = "operating_runtime" payload["policy_version"] = "pmflow-multi-agent-operating-runtime-v1" 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["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"), 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), 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], ) -> 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) terminal_state = delivery_payload.get("terminal_answer_state") if isinstance(delivery_payload.get("terminal_answer_state"), dict) else {} claims = _claim_texts(workspace, deliverable, terminal_state=terminal_state) 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 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]}, ], 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], "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 _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_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 _claim_texts( workspace: dict[str, Any], deliverable: dict[str, Any], *, terminal_state: 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(workspace.get("claim_ledger")) + _dicts(workspace.get("claims")): 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")): claims.append(str(item.get("summary") or item.get("claim_text") or item.get("text") or "").strip()) return _dedupe([item for item in claims if item and is_usable_research_claim_text(item)])[:12] 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 _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 _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], ) -> 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]], 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, *, project_world_state: PMFlowAIProjectWorldStatePacket, council_review: dict[str, Any], autonomy_gate: dict[str, Any], operating_cycle: dict[str, Any], 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 [] operating_steps = [ { "key": "operating_mission", "label": "建立目标:形成本轮 Operating Mission", "status": "completed", "display_summary": "已先确定目标、范围、成功标准和确认边界。", "duration_ms": None, "blocking": False, }, { "key": "specialist_agent_assignment", "label": "分派角色:组织专业 Agent Council", "status": "completed", "display_summary": "已分派策略、用户验证、产品、技术、执行和风险角色。", "duration_ms": None, "blocking": False, }, { "key": "project_world_state_read", "label": "读取项目世界状态:汇总项目运行面", "status": "completed" if project_world_state.status in {"ready", "partial"} else "blocked", "display_summary": _project_world_state_event_summary(project_world_state), "duration_ms": None, "blocking": project_world_state.status not in {"ready", "partial"}, }, { "key": "specialist_work_products", "label": "专业产出:生成六类 Agent Work Product", "status": "completed" if project_world_state.status in {"ready", "partial"} else "blocked", "display_summary": "已生成策略、用户验证、产品范围、技术架构、执行计划和风险审查产物。", "duration_ms": None, "blocking": project_world_state.status not in {"ready", "partial"}, }, { "key": "agent_council_review", "label": "会审计划:检查冲突、风险和优先级", "status": "completed" if council_review.get("status") != "blocked" else "blocked", "display_summary": str(council_review.get("resolution_summary") or "已完成 Agent Council 会审。")[:500], "duration_ms": None, "blocking": council_review.get("status") == "blocked", }, { "key": "operating_plan_compile", "label": "汇总计划:生成 Command Payload", "status": "completed", "display_summary": "已把 Council 会审结果编译为单一优先级、准备动作、确认项和边界。", "duration_ms": None, "blocking": False, }, { "key": "autonomy_gate_prepared_actions", "label": "确认门禁:准备动作包并阻断写入/执行", "status": "completed" if autonomy_gate.get("status") != "blocked" else "blocked", "display_summary": _autonomy_gate_process_summary(autonomy_gate), "duration_ms": None, "blocking": autonomy_gate.get("status") == "blocked", }, { "key": "learning_ledger_update", "label": "学习闭环:记录结果观察和下一轮触发", "status": "completed", "display_summary": _learning_process_summary(operating_cycle), "duration_ms": None, "blocking": False, }, ] packet["packet_version"] = "customer_answer_process.v1" packet["visibility"] = "customer" packet["answer_type"] = "operating_run" packet["route_display"] = "multi_agent_operating_runtime" packet["steps"] = operating_steps + existing_steps packet["total_duration_ms"] = max(0, int(total_duration_ms or packet.get("total_duration_ms") or 0)) return packet 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 _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 _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 project_world_state.primary_project_ref == "space:external_research" or research_product is not None: 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]] answer += " 已准备可确认动作:" + ";".join(item for item in labels if item) + "。" 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 = " ".join(str(value or "").strip().split())[:limit] if not text: return "" return text.replace("Project Memory", "项目资料").replace("handoff", "执行交接") 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] __all__ = ["PMFlowAIOperatingRunService"]