2581 lines
114 KiB
Python
2581 lines
114 KiB
Python
"""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"]
|