984 lines
46 KiB
Python
984 lines
46 KiB
Python
"""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"]
|