1156 lines
51 KiB
Python
1156 lines
51 KiB
Python
"""Durable Agent OS runtime for user-facing PMFlow AI runs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import math
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from ...models import (
|
|
PMFlowAgentArtifact,
|
|
PMFlowAgentMemory,
|
|
PMFlowAgentObservation,
|
|
PMFlowAgentRun,
|
|
PMFlowAgentStep,
|
|
PMFlowAgentVerification,
|
|
User,
|
|
)
|
|
from ...schemas.pmflow_agent_runtime import (
|
|
PMFlowAgentArtifactPacket,
|
|
PMFlowAgentMemoryPacket,
|
|
PMFlowAgentObservationPacket,
|
|
PMFlowAgentRunPacket,
|
|
PMFlowAgentStepPacket,
|
|
PMFlowAgentVerificationPacket,
|
|
customer_safe_pmflow_agent_run_packet,
|
|
new_pmflow_agent_id,
|
|
utc_now_iso,
|
|
)
|
|
from ...schemas.rag import RAGAskRequest
|
|
from ..runtime_audit_context import bind_agent_audit_context
|
|
|
|
|
|
PMFLOW_AGENT_OS_POLICY_VERSION = "pmflow-agent-os-v2"
|
|
|
|
|
|
class PMFlowAgentRuntimeService:
|
|
"""Own the PMFlow AI Agent Run truth source.
|
|
|
|
QARecord is kept as transcript/history only. The current run state, steps,
|
|
observations, artifacts, verifications and memories are written to Agent OS
|
|
tables first, then projected into the customer payload.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
db: AsyncSession,
|
|
*,
|
|
tenant_id: int,
|
|
delivery_runtime: Any | None = None,
|
|
environment_service: Any | None = None,
|
|
specialist_runtime: Any | None = None,
|
|
) -> None:
|
|
self.db = db
|
|
self.tenant_id = int(tenant_id or 1)
|
|
# The product entry now uses a dynamic observe-plan-act-critic-answer
|
|
# agent runtime. The argument name is kept for constructor compatibility;
|
|
# when present it must expose run_turn() and is treated as the dynamic
|
|
# agent delegate, not as a legacy answer override.
|
|
self.delivery_runtime = delivery_runtime
|
|
self.environment_service = environment_service
|
|
self._compat_specialist_runtime = specialist_runtime
|
|
|
|
async def run_space_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]:
|
|
run = PMFlowAgentRun(
|
|
tenant_id=self.tenant_id,
|
|
public_run_id=new_pmflow_agent_id("run"),
|
|
user_id=int(getattr(current_user, "id", 0) or 0) or None,
|
|
qa_session_id=_safe_int(getattr(request, "session_id", None)),
|
|
workspace_scope="space",
|
|
surface="space",
|
|
presentation_mode="pmflow_ai",
|
|
source_channel=str(getattr(request, "asked_via", "") or "pmflow")[:40],
|
|
source_ref=str(getattr(request, "source_chat_id", "") or "")[:160] or None,
|
|
title=_title_from_question(getattr(request, "question", "")),
|
|
goal=str(getattr(request, "question", "") or "").strip(),
|
|
status="active",
|
|
active_object_refs=[],
|
|
run_metadata={
|
|
"runtime": "pmflow_agent_os",
|
|
"runtime_contract": "agent_os_direct_answer_runtime",
|
|
"policy_version": PMFLOW_AGENT_OS_POLICY_VERSION,
|
|
"conversation_session_id": _safe_int((conversation_context or {}).get("session_id")),
|
|
},
|
|
started_at=datetime.utcnow(),
|
|
)
|
|
self.db.add(run)
|
|
await self.db.flush()
|
|
|
|
try:
|
|
run_kwargs = {
|
|
"request": request,
|
|
"current_user": current_user,
|
|
"conversation_context": conversation_context,
|
|
"tenant_project_context": tenant_project_context,
|
|
"started_at": started_at,
|
|
}
|
|
if event_sink is not None:
|
|
run_kwargs["event_sink"] = event_sink
|
|
with bind_agent_audit_context(agent_run_id=run.public_run_id):
|
|
agent_payload = await self._dynamic_agent_runtime().run_turn(**run_kwargs)
|
|
answer = str(agent_payload.get("answer") or agent_payload.get("reply_text") or "").strip()
|
|
agent_run_packet = _dict(agent_payload.get("agent_run_packet"))
|
|
run.active_object_refs = _agent_payload_object_refs(agent_payload)
|
|
run.status = _durable_status_from_agent_payload(agent_payload, answer=answer)
|
|
run.final_answer = answer.strip() or None
|
|
run.completed_at = datetime.utcnow()
|
|
run.run_metadata = {
|
|
**dict(run.run_metadata or {}),
|
|
"runtime": "pmflow_agent_os",
|
|
"runtime_contract": "agent_os_direct_answer_runtime",
|
|
"delegated_runtime": str(agent_payload.get("answer_mode") or agent_payload.get("processing_stage") or "pmflow_ai_agent_runtime"),
|
|
"delegated_run_id": str(agent_run_packet.get("run_id") or ""),
|
|
"step_count": len(list(agent_run_packet.get("steps") or [])),
|
|
"observation_count": len(list(agent_run_packet.get("observations") or [])),
|
|
"artifact_count": 2 if answer else 1,
|
|
"verification_count": len(_agent_verifications_from_payload(agent_payload, answer=answer)),
|
|
"stop_reason": str(agent_run_packet.get("stop_reason") or ""),
|
|
"iterations": _safe_int(agent_run_packet.get("iterations")),
|
|
"tool_call_count": _safe_int(agent_run_packet.get("tool_call_count")),
|
|
}
|
|
packet = await self._persist_agent_runtime_payload(
|
|
run=run,
|
|
agent_payload=agent_payload,
|
|
answer=answer,
|
|
)
|
|
payload = self._build_customer_payload_from_agent_runtime(
|
|
request=request,
|
|
run=run,
|
|
packet=packet,
|
|
agent_payload=agent_payload,
|
|
answer=answer,
|
|
)
|
|
await self.db.flush()
|
|
return payload
|
|
except Exception as exc:
|
|
run.status = "failed"
|
|
run.last_error = _customer_safe_error_summary(exc)
|
|
run.completed_at = datetime.utcnow()
|
|
await self.db.flush()
|
|
raise
|
|
|
|
def _dynamic_agent_runtime(self) -> Any:
|
|
if self.delivery_runtime is not None and callable(getattr(self.delivery_runtime, "run_turn", None)):
|
|
return self.delivery_runtime
|
|
from ..pmflow_ai_agent_runtime import PMFlowAIAgentRunService
|
|
|
|
return PMFlowAIAgentRunService(
|
|
self.db,
|
|
tenant_id=self.tenant_id,
|
|
)
|
|
|
|
async def _persist_agent_runtime_payload(
|
|
self,
|
|
*,
|
|
run: PMFlowAgentRun,
|
|
agent_payload: dict[str, Any],
|
|
answer: str,
|
|
) -> PMFlowAgentRunPacket:
|
|
steps = _steps_from_agent_payload(agent_payload)
|
|
step_rows_by_public_id: dict[str, PMFlowAgentStep] = {}
|
|
for step in steps:
|
|
row = PMFlowAgentStep(
|
|
tenant_id=self.tenant_id,
|
|
run_id=int(run.id),
|
|
public_step_id=step.step_id,
|
|
step_index=int(step.index),
|
|
step_kind=step.kind,
|
|
status=step.status,
|
|
input_summary=(run.goal or "")[:1000],
|
|
output_summary=step.summary,
|
|
tool_name=step.tool_name,
|
|
duration_ms=step.duration_ms,
|
|
step_payload=_storage_safe_json(step.model_dump(mode="json", exclude_none=True), string_limit=1200),
|
|
completed_at=datetime.utcnow(),
|
|
)
|
|
self.db.add(row)
|
|
step_rows_by_public_id[step.step_id] = row
|
|
await self.db.flush()
|
|
|
|
observations = _observations_from_agent_payload(agent_payload, steps=steps)
|
|
for observation in observations:
|
|
source_step = step_rows_by_public_id.get(str(observation.step_id or ""))
|
|
self.db.add(
|
|
PMFlowAgentObservation(
|
|
tenant_id=self.tenant_id,
|
|
run_id=int(run.id),
|
|
step_id=int(source_step.id) if source_step is not None and getattr(source_step, "id", None) is not None else None,
|
|
public_observation_id=observation.observation_id,
|
|
source_kind=observation.source_kind,
|
|
customer_safe_summary=observation.summary,
|
|
confidence=observation.confidence,
|
|
facts=list(_storage_safe_json(observation.facts or [], string_limit=1200, list_limit=32)),
|
|
gaps=list(_storage_safe_json(observation.gaps or [], string_limit=800, list_limit=16)),
|
|
raw_payload=_storage_safe_json(
|
|
observation.model_dump(mode="json", exclude_none=True),
|
|
string_limit=1200,
|
|
list_limit=32,
|
|
),
|
|
observed_at=datetime.utcnow(),
|
|
)
|
|
)
|
|
|
|
artifacts = _artifacts_from_agent_payload(agent_payload, answer=answer)
|
|
for artifact in artifacts:
|
|
self.db.add(
|
|
PMFlowAgentArtifact(
|
|
tenant_id=self.tenant_id,
|
|
run_id=int(run.id),
|
|
step_id=None,
|
|
public_artifact_id=artifact.artifact_id,
|
|
artifact_kind=artifact.kind,
|
|
title=artifact.title,
|
|
status=artifact.status,
|
|
payload=_storage_safe_json(dict(artifact.payload or {}), string_limit=2000, list_limit=24),
|
|
customer_safe_payload=_storage_safe_json(
|
|
artifact.model_dump(mode="json", exclude_none=True),
|
|
string_limit=2000,
|
|
list_limit=24,
|
|
),
|
|
created_by_step_id=steps[-1].step_id if steps else None,
|
|
)
|
|
)
|
|
|
|
verifications = _agent_verifications_from_payload(agent_payload, answer=answer)
|
|
for verification in verifications:
|
|
self.db.add(
|
|
PMFlowAgentVerification(
|
|
tenant_id=self.tenant_id,
|
|
run_id=int(run.id),
|
|
step_id=None,
|
|
public_verification_id=verification.verification_id,
|
|
target_ref=verification.target_ref,
|
|
result=verification.result,
|
|
retry_would_change_state=verification.retry_would_change_state,
|
|
findings=list(_storage_safe_json(verification.findings or [], string_limit=800, list_limit=16)),
|
|
verification_payload=_storage_safe_json(
|
|
verification.model_dump(mode="json", exclude_none=True),
|
|
string_limit=1200,
|
|
list_limit=16,
|
|
),
|
|
verified_at=datetime.utcnow(),
|
|
)
|
|
)
|
|
|
|
memories = _agent_memories_from_payload(agent_payload)
|
|
for memory in memories:
|
|
self.db.add(
|
|
PMFlowAgentMemory(
|
|
tenant_id=self.tenant_id,
|
|
run_id=int(run.id),
|
|
memory_kind=memory.memory_kind,
|
|
scope_ref=memory.scope_ref,
|
|
outcome_signal=memory.outcome_signal,
|
|
content={"summary": _storage_safe_text(memory.summary, limit=1000)},
|
|
memory_payload=_storage_safe_json(memory.model_dump(mode="json", exclude_none=True), string_limit=1000),
|
|
)
|
|
)
|
|
|
|
return PMFlowAgentRunPacket(
|
|
run_id=run.public_run_id,
|
|
status=run.status,
|
|
goal=run.goal,
|
|
surface=run.surface or "space",
|
|
presentation_mode=run.presentation_mode or "pmflow_ai",
|
|
steps=steps,
|
|
observations=observations,
|
|
artifacts=artifacts,
|
|
verifications=verifications,
|
|
memories=memories,
|
|
command_payload=_dict(agent_payload.get("command_payload") or agent_payload.get("answer_canvas")),
|
|
created_at=utc_now_iso(),
|
|
completed_at=utc_now_iso(),
|
|
)
|
|
|
|
def _build_customer_payload_from_agent_runtime(
|
|
self,
|
|
*,
|
|
request: RAGAskRequest,
|
|
run: PMFlowAgentRun,
|
|
packet: PMFlowAgentRunPacket,
|
|
agent_payload: dict[str, Any],
|
|
answer: str,
|
|
) -> dict[str, Any]:
|
|
safe_packet = customer_safe_pmflow_agent_run_packet(packet)
|
|
process_packet = _dict(agent_payload.get("answer_process_packet"))
|
|
answer_canvas = _dict(agent_payload.get("answer_canvas")) or _answer_canvas(answer=answer, packet=packet)
|
|
customer_evidence_packet = _dict(agent_payload.get("customer_evidence_packet")) or _customer_evidence_packet(
|
|
answer=answer,
|
|
packet=packet,
|
|
)
|
|
deliverable_packet = _dict(agent_payload.get("deliverable_packet"))
|
|
answer_runtime_packet = _dict(agent_payload.get("answer_runtime_packet"))
|
|
terminal_state = _dict(agent_payload.get("terminal_answer_state"))
|
|
payload = {
|
|
"answer": answer,
|
|
"reply_text": answer,
|
|
"final_answer": answer,
|
|
"run_id": run.public_run_id,
|
|
"sources": list(agent_payload.get("sources") or []),
|
|
"sources_used": list(agent_payload.get("sources_used") or agent_payload.get("sources") or []),
|
|
"thinking_mode": getattr(request, "thinking_mode", None) or "normal",
|
|
"processing_stage": "pmflow_agent_runtime",
|
|
"answer_mode": "pmflow_agent_runtime",
|
|
"grounding_status": str(agent_payload.get("grounding_status") or ("grounded" if packet.observations else "partially_grounded")),
|
|
"policy_version": PMFLOW_AGENT_OS_POLICY_VERSION,
|
|
"pmflow_agent_run": safe_packet,
|
|
"agent_os_environment": {
|
|
"packet_version": "pmflow_agent_environment.v1",
|
|
"visibility": "customer",
|
|
"steps": safe_packet.get("steps") or [],
|
|
"observations": safe_packet.get("observations") or [],
|
|
},
|
|
"agent_os_run_id": run.public_run_id,
|
|
"agent_os_steps": safe_packet.get("steps") or [],
|
|
"agent_os_observations": safe_packet.get("observations") or [],
|
|
"agent_os_artifacts": safe_packet.get("artifacts") or [],
|
|
"agent_os_verifications": safe_packet.get("verifications") or [],
|
|
"agent_os_memories": safe_packet.get("memories") or [],
|
|
"operating_mission": _dict(agent_payload.get("operating_mission")),
|
|
"project_world_state_packet": _dict(agent_payload.get("project_world_state_packet")),
|
|
"operating_run_packet": _dict(agent_payload.get("operating_run_packet")),
|
|
"agent_os_specialists": {"runtime": "mission_first_operating_runtime"} if _dict(agent_payload.get("operating_mission")) else {},
|
|
"agent_os_specialist_assignments": list(agent_payload.get("agent_assignments") or []),
|
|
"agent_os_work_products": list(agent_payload.get("agent_work_products") or []),
|
|
"agent_os_agent_council_review": _dict(agent_payload.get("agent_council_review")),
|
|
"agent_os_operating_plan": _dict(agent_payload.get("operating_plan")),
|
|
"agent_os_autonomy_gate": _dict(agent_payload.get("autonomy_gate")),
|
|
"agent_os_operating_cycle": _dict(agent_payload.get("operating_cycle")),
|
|
"agent_os_consumed_learning_ledger": _dict(agent_payload.get("consumed_learning_ledger")),
|
|
"agent_os_outcome_observations": list(agent_payload.get("outcome_observations") or []),
|
|
"agent_os_learning_ledger": _dict(agent_payload.get("learning_ledger")),
|
|
"agent_os_learning_updates": list(agent_payload.get("learning_updates") or []),
|
|
"agent_os_next_cycle_triggers": list(agent_payload.get("next_cycle_triggers") or []),
|
|
"agent_os_prepared_action_packets": _prepared_actions_from_agent_payload(agent_payload),
|
|
"agent_os_confirmation_requests": _confirmation_requests_from_agent_payload(agent_payload),
|
|
"agent_os_execution_preparations": list(agent_payload.get("execution_preparations") or []),
|
|
"agent_os_product_object_projections": list(agent_payload.get("product_object_projections") or []),
|
|
"agent_os_command_payload": _dict(agent_payload.get("command_payload")),
|
|
"agent_assignments": list(agent_payload.get("agent_assignments") or []),
|
|
"agent_work_products": list(agent_payload.get("agent_work_products") or []),
|
|
"agent_council_review": _dict(agent_payload.get("agent_council_review")),
|
|
"autonomy_gate": _dict(agent_payload.get("autonomy_gate")),
|
|
"prepared_action_packets": _prepared_actions_from_agent_payload(agent_payload),
|
|
"confirmation_requests": _confirmation_requests_from_agent_payload(agent_payload),
|
|
"execution_preparations": list(agent_payload.get("execution_preparations") or []),
|
|
"product_object_projections": list(agent_payload.get("product_object_projections") or []),
|
|
"operating_cycle": _dict(agent_payload.get("operating_cycle")),
|
|
"consumed_learning_ledger": _dict(agent_payload.get("consumed_learning_ledger")),
|
|
"outcome_observations": list(agent_payload.get("outcome_observations") or []),
|
|
"learning_ledger": _dict(agent_payload.get("learning_ledger")),
|
|
"learning_updates": list(agent_payload.get("learning_updates") or []),
|
|
"next_cycle_triggers": list(agent_payload.get("next_cycle_triggers") or []),
|
|
"operating_plan": _dict(agent_payload.get("operating_plan")),
|
|
"command_payload": _dict(agent_payload.get("command_payload")),
|
|
"answer_process_packet": process_packet or _answer_process_packet(packet),
|
|
"customer_evidence_packet": customer_evidence_packet,
|
|
"answer_canvas": answer_canvas,
|
|
"deliverable_packet": deliverable_packet,
|
|
"answer_runtime_packet": answer_runtime_packet,
|
|
"terminal_answer_state": terminal_state,
|
|
"turn_presentation_contract": _dict(agent_payload.get("turn_presentation_contract")),
|
|
"customer_interaction_context_packet": _customer_interaction_context_from_agent_payload(agent_payload),
|
|
# Kept for customer payload sanitizers and debug-only product gates.
|
|
# The PMFlow AI workspace does not render these raw packets.
|
|
"agent_run_packet": _dict(agent_payload.get("agent_run_packet")),
|
|
"research_workspace": _dict(agent_payload.get("research_workspace")),
|
|
}
|
|
return payload
|
|
|
|
|
|
def _answer_process_packet(packet: PMFlowAgentRunPacket) -> dict[str, Any]:
|
|
return {
|
|
"packet_version": "customer_answer_process.v1",
|
|
"visibility": "customer",
|
|
"answer_type": "pmflow_agent_runtime",
|
|
"route_display": "pmflow_agent_runtime",
|
|
"steps": [
|
|
{
|
|
"key": step.kind,
|
|
"label": step.label,
|
|
"status": step.status,
|
|
"display_summary": step.summary,
|
|
"duration_ms": step.duration_ms,
|
|
}
|
|
for step in packet.steps
|
|
],
|
|
"total_duration_ms": sum(int(step.duration_ms or 0) for step in packet.steps),
|
|
}
|
|
|
|
|
|
def _steps_from_agent_payload(agent_payload: dict[str, Any]) -> list[PMFlowAgentStepPacket]:
|
|
agent_run = _dict(agent_payload.get("agent_run_packet"))
|
|
rows = list(agent_run.get("steps") or [])
|
|
if not rows:
|
|
process = _dict(agent_payload.get("answer_process_packet"))
|
|
rows = list(process.get("steps") or [])
|
|
steps: list[PMFlowAgentStepPacket] = []
|
|
for index, row in enumerate(rows[:32]):
|
|
if not isinstance(row, dict):
|
|
continue
|
|
step_key = str(row.get("key") or row.get("step_id") or f"agent_step_{index + 1}").strip()[:80]
|
|
selected_tool = str(row.get("selected_tool") or row.get("tool_name") or "").strip()[:120] or None
|
|
step_type = str(row.get("step_type") or row.get("type") or row.get("key") or "agent_step").strip()[:64]
|
|
kind = selected_tool if step_type == "tool" and selected_tool else step_type
|
|
label = str(row.get("label") or _agent_step_label(kind) or f"推进第 {index + 1} 步").strip()[:160]
|
|
summary = str(row.get("display_summary") or row.get("summary") or row.get("description") or "").strip()[:1000]
|
|
status = _durable_step_status(row.get("status"), blocking=bool(row.get("blocking")))
|
|
steps.append(
|
|
PMFlowAgentStepPacket(
|
|
step_id=str(row.get("step_id") or step_key or new_pmflow_agent_id("step"))[:80],
|
|
index=index,
|
|
kind=kind or "agent_step",
|
|
status=status,
|
|
label=label,
|
|
summary=summary or label,
|
|
tool_name=selected_tool,
|
|
duration_ms=_safe_int(row.get("duration_ms")),
|
|
)
|
|
)
|
|
if not steps:
|
|
steps.append(
|
|
PMFlowAgentStepPacket(
|
|
step_id=new_pmflow_agent_id("step"),
|
|
index=0,
|
|
kind="agent_answer_runtime",
|
|
status="completed" if str(agent_payload.get("answer") or "").strip() else "blocked",
|
|
label="生成回答",
|
|
summary="已根据本轮客户安全状态组织回答。",
|
|
)
|
|
)
|
|
return _dedupe_steps(steps)
|
|
|
|
|
|
def _observations_from_agent_payload(agent_payload: dict[str, Any], *, steps: list[PMFlowAgentStepPacket]) -> list[PMFlowAgentObservationPacket]:
|
|
agent_run = _dict(agent_payload.get("agent_run_packet"))
|
|
rows = list(agent_run.get("observations") or [])
|
|
if not rows:
|
|
workspace = _dict(agent_payload.get("agent_workspace"))
|
|
rows = list(workspace.get("observations") or [])
|
|
step_id_by_observation_id = {
|
|
str(row.get("observation_id")): str(row.get("step_id"))
|
|
for row in list(agent_run.get("steps") or [])
|
|
if isinstance(row, dict) and row.get("observation_id") and row.get("step_id")
|
|
}
|
|
tool_steps = [step for step in steps if step.tool_name]
|
|
fallback_step_id = (tool_steps[-1].step_id if tool_steps else (steps[-1].step_id if steps else None))
|
|
observations: list[PMFlowAgentObservationPacket] = []
|
|
for index, row in enumerate(rows[:48]):
|
|
if not isinstance(row, dict):
|
|
continue
|
|
facts = [
|
|
_storage_safe_json(dict(item), string_limit=1200, list_limit=12, dict_limit=32)
|
|
for item in list(row.get("facts") or [])[:32]
|
|
if isinstance(item, dict)
|
|
]
|
|
observation_id = str(row.get("observation_id") or new_pmflow_agent_id("obs"))[:80]
|
|
observations.append(
|
|
PMFlowAgentObservationPacket(
|
|
observation_id=observation_id,
|
|
step_id=str(row.get("step_id") or step_id_by_observation_id.get(observation_id) or fallback_step_id or "")[:80] or None,
|
|
source_kind=str(row.get("source") or row.get("source_kind") or "agent_runtime")[:80],
|
|
summary=_storage_safe_text(row.get("summary"), limit=1200),
|
|
confidence=_storage_safe_text(row.get("confidence") or "unknown", limit=32),
|
|
facts=facts,
|
|
gaps=_gaps_from_agent_observation(row, facts=facts),
|
|
)
|
|
)
|
|
if not observations:
|
|
observations.append(
|
|
PMFlowAgentObservationPacket(
|
|
step_id=fallback_step_id,
|
|
source_kind="agent_runtime",
|
|
summary="本轮没有产生额外工具观察。",
|
|
confidence="unknown",
|
|
facts=[],
|
|
gaps=[],
|
|
)
|
|
)
|
|
return _dedupe_observations(observations)
|
|
|
|
|
|
def _artifacts_from_agent_payload(agent_payload: dict[str, Any], *, answer: str) -> list[PMFlowAgentArtifactPacket]:
|
|
artifacts: list[PMFlowAgentArtifactPacket] = []
|
|
for projection in list(agent_payload.get("product_object_projections") or [])[:8]:
|
|
if not isinstance(projection, dict):
|
|
continue
|
|
artifacts.append(
|
|
PMFlowAgentArtifactPacket(
|
|
kind="product_object_projection",
|
|
status=str(projection.get("status") or "ready_for_review")[:32],
|
|
title=str(projection.get("title") or projection.get("object_type") or "经营对象候选")[:240],
|
|
summary=str(projection.get("summary") or "")[:1000],
|
|
payload=projection,
|
|
)
|
|
)
|
|
for product in list(agent_payload.get("agent_work_products") or [])[:12]:
|
|
if not isinstance(product, dict):
|
|
continue
|
|
artifacts.append(
|
|
PMFlowAgentArtifactPacket(
|
|
kind="specialist_work_product",
|
|
status=str(product.get("status") or "completed")[:32],
|
|
title=_customer_work_product_title(product),
|
|
summary=str(product.get("summary") or "")[:1000],
|
|
payload=product,
|
|
)
|
|
)
|
|
deliverable = _dict(agent_payload.get("deliverable_packet"))
|
|
if deliverable:
|
|
artifacts.append(
|
|
PMFlowAgentArtifactPacket(
|
|
kind="agent_deliverable",
|
|
status=str(deliverable.get("status") or "completed")[:32],
|
|
title=str(deliverable.get("title") or "本轮结果")[:240],
|
|
summary=str(deliverable.get("summary") or deliverable.get("answer_text") or "")[:1000],
|
|
payload=deliverable,
|
|
)
|
|
)
|
|
research_workspace = _dict(agent_payload.get("research_workspace"))
|
|
if research_workspace and (research_workspace.get("candidate_items") or research_workspace.get("research_goal")):
|
|
artifacts.append(
|
|
PMFlowAgentArtifactPacket(
|
|
kind="research_workspace",
|
|
status="completed",
|
|
title=str(research_workspace.get("topic") or "研究工作区")[:240],
|
|
summary=str(research_workspace.get("research_goal") or research_workspace.get("topic") or "")[:1000],
|
|
payload=research_workspace,
|
|
)
|
|
)
|
|
quality_findings = list(_dict(agent_payload.get("answer_runtime_packet")).get("quality_findings") or [])
|
|
artifacts.append(_final_answer_artifact(answer, status="blocked" if quality_findings else None))
|
|
return _dedupe_artifacts(artifacts)
|
|
|
|
|
|
def _agent_verifications_from_payload(agent_payload: dict[str, Any], *, answer: str) -> list[PMFlowAgentVerificationPacket]:
|
|
agent_run = _dict(agent_payload.get("agent_run_packet"))
|
|
events = [item for item in list(agent_run.get("event_log") or []) if isinstance(item, dict)]
|
|
verifications: list[PMFlowAgentVerificationPacket] = []
|
|
for event in events[:40]:
|
|
event_type = str(event.get("event_type") or "").strip()
|
|
if event_type not in {"critic_review", "runtime_stop"}:
|
|
continue
|
|
is_stop = event_type == "runtime_stop"
|
|
stop_reason = str(event.get("stop_reason") or "").strip()
|
|
blocked = is_stop and stop_reason not in {"ready_to_answer", "planner_answer"}
|
|
verifications.append(
|
|
PMFlowAgentVerificationPacket(
|
|
target_ref=f"{event_type}:{event.get('iteration', 0)}",
|
|
result="blocked" if blocked else "passed",
|
|
retry_would_change_state=bool(event.get("state_changed") or blocked),
|
|
findings=[
|
|
{
|
|
"type": stop_reason or event_type,
|
|
"summary": str(event.get("summary") or stop_reason or event_type)[:500],
|
|
}
|
|
],
|
|
)
|
|
)
|
|
quality_findings = list(_dict(agent_payload.get("answer_runtime_packet")).get("quality_findings") or [])
|
|
verifications.append(
|
|
PMFlowAgentVerificationPacket(
|
|
target_ref="final_answer",
|
|
result="passed" if answer.strip() and not quality_findings else "blocked",
|
|
retry_would_change_state=False,
|
|
findings=[
|
|
{"type": str(item or "quality_finding")[:120], "summary": "回答出口命中质量门禁。"}
|
|
for item in quality_findings[:8]
|
|
],
|
|
)
|
|
)
|
|
return _dedupe_verifications(verifications)
|
|
|
|
|
|
def _agent_memories_from_payload(agent_payload: dict[str, Any]) -> list[PMFlowAgentMemoryPacket]:
|
|
learning = _dict(agent_payload.get("learning_ledger"))
|
|
learning_items = list(learning.get("learning_items") or [])
|
|
memories: list[PMFlowAgentMemoryPacket] = []
|
|
for item in learning_items[:8]:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
memories.append(
|
|
PMFlowAgentMemoryPacket(
|
|
memory_kind="operating_learning",
|
|
scope_ref=str(learning.get("scope_ref") or item.get("scope_ref") or "")[:160] or None,
|
|
outcome_signal=str(item.get("outcome_status") or item.get("learning_key") or "observed")[:80],
|
|
summary=str(item.get("summary") or item.get("learning_key") or "")[:1000],
|
|
)
|
|
)
|
|
if memories:
|
|
return _dedupe_memories(memories)
|
|
workspace = _dict(agent_payload.get("agent_workspace"))
|
|
deliverable = _dict(agent_payload.get("deliverable_packet"))
|
|
summary = str(deliverable.get("summary") or workspace.get("current_goal") or "").strip()
|
|
if not summary:
|
|
return []
|
|
return [
|
|
PMFlowAgentMemoryPacket(
|
|
memory_kind="agent_workspace_snapshot",
|
|
scope_ref=str(workspace.get("workspace_id") or "")[:160] or None,
|
|
outcome_signal=str(deliverable.get("status") or workspace.get("status") or "observed")[:80],
|
|
summary=summary[:1000],
|
|
)
|
|
]
|
|
|
|
|
|
def _gaps_from_agent_observation(row: dict[str, Any], *, facts: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
gaps: list[dict[str, Any]] = []
|
|
observation_type = str(row.get("observation_type") or "").strip()
|
|
if observation_type in {"tool_rejected", "external_search_failed", "external_search_missing_query", "project_world_state_missing_scope"}:
|
|
gaps.append(
|
|
{
|
|
"type": observation_type,
|
|
"summary": str(row.get("summary") or "本轮观察未完整完成。")[:500],
|
|
"user_actionable": False,
|
|
}
|
|
)
|
|
for fact in facts[:16]:
|
|
fact_type = str(fact.get("type") or "").strip()
|
|
if fact_type.startswith("missing_") or fact_type in {"tool_rejected", "tool_error", "no_object_candidates"}:
|
|
gaps.append(
|
|
{
|
|
"type": fact_type,
|
|
"summary": str(fact.get("summary") or fact.get("reason") or row.get("summary") or fact_type)[:500],
|
|
"user_actionable": bool(fact.get("user_actionable", False)),
|
|
}
|
|
)
|
|
return gaps[:12]
|
|
|
|
|
|
def _agent_payload_object_refs(agent_payload: dict[str, Any]) -> list[str]:
|
|
refs: list[str] = []
|
|
mission = _dict(agent_payload.get("operating_mission"))
|
|
scope = _dict(mission.get("project_scope"))
|
|
for item in list(scope.get("object_refs") or []):
|
|
if isinstance(item, str) and item.strip():
|
|
refs.append(item.strip()[:160])
|
|
for item in list(mission.get("target_objects") or []):
|
|
if isinstance(item, dict):
|
|
object_ref = str(item.get("object_ref") or "").strip()
|
|
if object_ref:
|
|
refs.append(object_ref[:160])
|
|
for source in (
|
|
_dict(agent_payload.get("deliverable_packet")).get("object_refs"),
|
|
_dict(agent_payload.get("answer_canvas")).get("object_refs"),
|
|
):
|
|
for item in list(source or []):
|
|
if isinstance(item, str) and item.strip():
|
|
refs.append(item.strip()[:160])
|
|
elif isinstance(item, dict):
|
|
object_ref = str(item.get("object_ref") or "").strip()
|
|
if object_ref:
|
|
refs.append(object_ref[:160])
|
|
workspace = _dict(agent_payload.get("agent_workspace"))
|
|
for item in list(workspace.get("object_ledger") or [])[:12]:
|
|
if isinstance(item, dict):
|
|
object_ref = str(item.get("object_ref") or "").strip()
|
|
if object_ref:
|
|
refs.append(object_ref[:160])
|
|
return _dedupe([item for item in refs if item])[:12]
|
|
|
|
|
|
def _customer_interaction_context_from_agent_payload(agent_payload: dict[str, Any]) -> dict[str, Any]:
|
|
refs = _agent_payload_object_refs(agent_payload)
|
|
objects: list[dict[str, Any]] = []
|
|
workspace = _dict(agent_payload.get("agent_workspace"))
|
|
for item in list(workspace.get("object_ledger") or [])[:12]:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
object_ref = str(item.get("object_ref") or "").strip()
|
|
if not object_ref:
|
|
continue
|
|
objects.append(
|
|
{
|
|
"object_ref": object_ref[:160],
|
|
"object_type": str(item.get("object_type") or object_ref.split(":", 1)[0] or "object")[:80],
|
|
"title": str(item.get("title") or object_ref)[:240],
|
|
"binding_strength": str(item.get("binding_strength") or "candidate")[:80],
|
|
}
|
|
)
|
|
if not objects:
|
|
objects = [
|
|
{
|
|
"object_ref": ref,
|
|
"object_type": ref.split(":", 1)[0] if ":" in ref else "object",
|
|
"title": ref,
|
|
"binding_strength": "derived_from_agent_output",
|
|
}
|
|
for ref in refs
|
|
]
|
|
return {
|
|
"packet_version": "customer_interaction_context.v1",
|
|
"visibility": "customer",
|
|
"current_focus_object": objects[0] if objects else None,
|
|
"last_shown_objects": objects[:12],
|
|
"recent_objects": objects[:12],
|
|
}
|
|
|
|
|
|
def _prepared_actions_from_agent_payload(agent_payload: dict[str, Any]) -> list[dict[str, Any]]:
|
|
operating_rows = agent_payload.get("prepared_action_packets")
|
|
if isinstance(operating_rows, list) and operating_rows:
|
|
out: list[dict[str, Any]] = []
|
|
for index, item in enumerate(operating_rows[:8], start=1):
|
|
if not isinstance(item, dict):
|
|
continue
|
|
title = str(item.get("title") or "").strip()
|
|
if not _is_customer_visible_action_title(title):
|
|
continue
|
|
out.append(
|
|
{
|
|
"action_id": str(item.get("prepared_action_id") or f"operating_action_{index}")[:80],
|
|
"title": title[:120],
|
|
"summary": _customer_visible_action_detail(
|
|
item.get("summary")
|
|
or _dict(item.get("payload")).get("title")
|
|
or "已准备为待确认动作,确认前不会写入或执行。"
|
|
),
|
|
"href": str(item.get("href") or "")[:300],
|
|
"target_ref": str(item.get("target_ref") or "")[:160] or None,
|
|
"status": str(item.get("status") or "prepared_waiting_confirmation")[:80],
|
|
"confirmation_required": bool(item.get("confirmation_required", True)),
|
|
"source": "operating_prepared_action",
|
|
}
|
|
)
|
|
return out
|
|
canvas = _dict(agent_payload.get("answer_canvas"))
|
|
rows = canvas.get("action_candidates")
|
|
if not isinstance(rows, list):
|
|
rows = []
|
|
answer_text = str(agent_payload.get("answer") or agent_payload.get("reply_text") or canvas.get("direct_answer") or "").strip()
|
|
valid_rows = [
|
|
item for item in rows
|
|
if isinstance(item, dict)
|
|
and _is_customer_visible_action_title(item.get("title"))
|
|
and str(item.get("href") or "").strip()
|
|
]
|
|
if len(valid_rows) > 1:
|
|
mentioned = [
|
|
item for item in valid_rows
|
|
if isinstance(item, dict)
|
|
and str(item.get("title") or "").strip()
|
|
and str(item.get("title") or "").strip() in answer_text
|
|
]
|
|
rows = mentioned if mentioned else valid_rows[:1]
|
|
else:
|
|
rows = valid_rows
|
|
out: list[dict[str, Any]] = []
|
|
for index, item in enumerate(rows[:8], start=1):
|
|
if not isinstance(item, dict):
|
|
continue
|
|
title = str(item.get("title") or "").strip()
|
|
row = dict(item)
|
|
href = str(row.get("href") or "").strip()[:300]
|
|
if not _is_customer_visible_action_title(title) or not href:
|
|
continue
|
|
summary = _customer_visible_action_detail(row.get("reason") or row.get("completion_criteria") or row.get("summary"))
|
|
out.append(
|
|
{
|
|
"action_id": str(row.get("action_id") or f"agent_action_{index}")[:80],
|
|
"title": title[:120],
|
|
"summary": summary,
|
|
"href": href,
|
|
"target_ref": str(row.get("target_ref") or row.get("object_ref") or "")[:160] or None,
|
|
"status": "prepared_waiting_confirmation" if bool(row.get("requires_confirmation") or row.get("requires_user_confirm")) else "suggested",
|
|
"confirmation_required": bool(row.get("requires_confirmation") or row.get("requires_user_confirm")),
|
|
"source": "deliverable_action_candidate",
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def _is_customer_visible_action_title(value: object) -> bool:
|
|
text = " ".join(str(value or "").strip().split())
|
|
if not text or len(text) > 120:
|
|
return False
|
|
lowered = text.lower()
|
|
internal_terms = (
|
|
"goalframe",
|
|
"top_next_actions",
|
|
"turnstate",
|
|
"claim ledger",
|
|
"boundary ledger",
|
|
"agent_os",
|
|
"runtime",
|
|
"planner",
|
|
"critic",
|
|
"workspace",
|
|
"target_ref",
|
|
"object_ref",
|
|
"tool",
|
|
)
|
|
if any(term in lowered for term in internal_terms):
|
|
return False
|
|
if re.search(r"\b(project|approval|change_set|task):\d+\b", lowered):
|
|
return False
|
|
if ";对象" in text or ";依据" in text or "; object" in lowered:
|
|
return False
|
|
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")
|
|
return not (latin_count > 20 and cjk_count == 0)
|
|
|
|
|
|
def _customer_visible_action_detail(value: object) -> str:
|
|
text = " ".join(str(value or "").strip().split())[:240]
|
|
if not text:
|
|
return ""
|
|
lowered = text.lower()
|
|
if any(term in lowered for term in ("goalframe", "top_next_actions", "turnstate", "target_ref", "object_ref")):
|
|
return ""
|
|
if re.search(r"\b(project|approval|change_set|task):\d+\b", lowered):
|
|
return ""
|
|
return text
|
|
|
|
|
|
def _customer_work_product_title(product: dict[str, Any]) -> str:
|
|
labels = {
|
|
"IdeaSeed": "想法种子",
|
|
"ResearchNote": "研究记录",
|
|
"CompetitorMap": "竞品地图",
|
|
"OpportunityBrief": "机会简报",
|
|
"MVPOption": "MVP 选项",
|
|
"TaskDraft": "任务草案",
|
|
"TechnicalArchitectureAssessment": "技术评估",
|
|
"QAPlan": "质量与风险计划",
|
|
"GrowthExperiment": "增长实验",
|
|
"RevenueSignal": "商业化信号",
|
|
"LearningUpdate": "学习记录",
|
|
}
|
|
product_type = str(product.get("product_type") or "").strip()
|
|
return labels.get(product_type, "专业 Agent 产物")[:240]
|
|
|
|
|
|
def _confirmation_requests_from_agent_payload(agent_payload: dict[str, Any]) -> list[dict[str, Any]]:
|
|
operating_rows = agent_payload.get("confirmation_requests")
|
|
if isinstance(operating_rows, list) and operating_rows:
|
|
out: list[dict[str, Any]] = []
|
|
for index, item in enumerate(operating_rows[:8], start=1):
|
|
if not isinstance(item, dict):
|
|
continue
|
|
out.append(
|
|
{
|
|
"confirmation_id": str(item.get("confirmation_request_id") or f"operating_confirmation_{index}")[:80],
|
|
"title": str(item.get("title") or "需要确认的动作")[:240],
|
|
"summary": str(item.get("confirmation_reason") or item.get("summary") or "")[:500],
|
|
"status": str(item.get("status") or "waiting_for_user_confirmation")[:80],
|
|
"requested_for": [str(value)[:120] for value in list(item.get("requested_for") or []) if str(value).strip()][:8],
|
|
"target_ref": str(item.get("target_ref") or "")[:160],
|
|
"action_label": str(item.get("action_label") or "")[:120],
|
|
"action_type": str(item.get("action_type") or "")[:120],
|
|
"action_payload": dict(item.get("action_payload") or {}) if isinstance(item.get("action_payload"), dict) else {},
|
|
"can_execute_after_confirmation": bool(item.get("can_execute_after_confirmation")),
|
|
}
|
|
)
|
|
return out
|
|
deliverable = _dict(agent_payload.get("deliverable_packet"))
|
|
rows = deliverable.get("pending_confirmations") if isinstance(deliverable.get("pending_confirmations"), list) else []
|
|
out: list[dict[str, Any]] = []
|
|
for index, item in enumerate(rows[:8], start=1):
|
|
if not isinstance(item, dict):
|
|
continue
|
|
out.append(
|
|
{
|
|
"confirmation_id": str(item.get("confirmation_id") or f"agent_confirmation_{index}")[:80],
|
|
"title": str(item.get("title") or item.get("summary") or "需要确认的动作")[:240],
|
|
"summary": str(item.get("summary") or item.get("description") or "")[:500],
|
|
"status": str(item.get("status") or "waiting_for_user_confirmation")[:80],
|
|
"action_label": str(item.get("action_label") or "")[:120],
|
|
"action_type": str(item.get("action_type") or "")[:120],
|
|
"action_payload": dict(item.get("action_payload") or {}) if isinstance(item.get("action_payload"), dict) else {},
|
|
"can_execute_after_confirmation": bool(item.get("can_execute_after_confirmation")),
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def _durable_status_from_agent_payload(agent_payload: dict[str, Any], *, answer: str) -> str:
|
|
if not answer.strip():
|
|
return "blocked"
|
|
agent_status = str(_dict(agent_payload.get("agent_run_packet")).get("status") or "").strip()
|
|
deliverable_status = str(_dict(agent_payload.get("deliverable_packet")).get("status") or "").strip()
|
|
if agent_status in {"blocked", "waiting_for_user"} or deliverable_status in {"blocked", "needs_customer_input"}:
|
|
return "blocked"
|
|
return "completed"
|
|
|
|
|
|
def _durable_step_status(value: Any, *, blocking: bool = False) -> str:
|
|
if blocking:
|
|
return "blocked"
|
|
text = str(value or "").strip().lower()
|
|
if text in {"completed", "done", "passed"}:
|
|
return "completed"
|
|
if text in {"active", "running", "pending"}:
|
|
return "active"
|
|
if text in {"blocked", "failed"}:
|
|
return "blocked"
|
|
if text in {"skipped"}:
|
|
return "skipped"
|
|
return "completed"
|
|
|
|
|
|
def _agent_step_label(kind: str) -> str:
|
|
labels = {
|
|
"understand_goal": "承接目标",
|
|
"workspace_snapshot": "建立工作区",
|
|
"planner": "规划下一步",
|
|
"critic": "检查状态",
|
|
"answer": "生成回答",
|
|
"external_search": "读取外部公开信息",
|
|
"inspect_space_collections": "读取空间状态",
|
|
"inspect_project_operating_state": "读取项目运行状态",
|
|
"inspect_pmflow_capability_boundary": "读取能力边界",
|
|
"inspect_saved_ideas": "读取暂存想法",
|
|
"inspect_attention_queue": "读取注意力队列",
|
|
"inspect_object_affordances": "读取对象可用动作",
|
|
"inspect_project_execution_package": "读取项目执行包",
|
|
"inspect_source_detail": "读取公开来源详情",
|
|
"resolve_customer_object": "解析对象锚点",
|
|
"inspect_object_detail": "读取对象详情",
|
|
}
|
|
return labels.get(str(kind or "").strip(), "")
|
|
|
|
|
|
def _storage_safe_text(value: Any, *, limit: int) -> str:
|
|
text = str(value or "")
|
|
if not text:
|
|
return ""
|
|
# PostgreSQL jsonb/text cannot store NUL bytes. External pages can contain
|
|
# copied control characters, so normalize once at the Agent OS persistence
|
|
# boundary instead of letting a single bad source roll back the whole turn.
|
|
text = text.replace("\x00", "")
|
|
text = text.encode("utf-8", errors="replace").decode("utf-8", errors="replace")
|
|
return " ".join(text.strip().split())[:limit]
|
|
|
|
|
|
def _storage_safe_json(
|
|
value: Any,
|
|
*,
|
|
string_limit: int = 2000,
|
|
list_limit: int = 24,
|
|
dict_limit: int = 64,
|
|
depth: int = 0,
|
|
max_depth: int = 5,
|
|
) -> Any:
|
|
if value is None or isinstance(value, (bool, int)):
|
|
return value
|
|
if isinstance(value, float):
|
|
return value if math.isfinite(value) else None
|
|
if isinstance(value, str):
|
|
return _storage_safe_text(value, limit=string_limit)
|
|
if isinstance(value, datetime):
|
|
return value.isoformat()
|
|
if depth >= max_depth:
|
|
return _storage_safe_text(value, limit=min(string_limit, 500))
|
|
if isinstance(value, dict):
|
|
out: dict[str, Any] = {}
|
|
for raw_key, raw_item in list(value.items())[:dict_limit]:
|
|
key = _storage_safe_text(raw_key, limit=120)
|
|
if not key:
|
|
continue
|
|
out[key] = _storage_safe_json(
|
|
raw_item,
|
|
string_limit=string_limit,
|
|
list_limit=list_limit,
|
|
dict_limit=dict_limit,
|
|
depth=depth + 1,
|
|
max_depth=max_depth,
|
|
)
|
|
return out
|
|
if isinstance(value, (list, tuple, set)):
|
|
return [
|
|
_storage_safe_json(
|
|
item,
|
|
string_limit=string_limit,
|
|
list_limit=list_limit,
|
|
dict_limit=dict_limit,
|
|
depth=depth + 1,
|
|
max_depth=max_depth,
|
|
)
|
|
for item in list(value)[:list_limit]
|
|
]
|
|
dump = getattr(value, "model_dump", None)
|
|
if callable(dump):
|
|
try:
|
|
return _storage_safe_json(
|
|
dump(mode="json", exclude_none=True),
|
|
string_limit=string_limit,
|
|
list_limit=list_limit,
|
|
dict_limit=dict_limit,
|
|
depth=depth + 1,
|
|
max_depth=max_depth,
|
|
)
|
|
except Exception:
|
|
return _storage_safe_text(value, limit=min(string_limit, 500))
|
|
return _storage_safe_text(value, limit=min(string_limit, 500))
|
|
|
|
|
|
def _dict(value: Any) -> dict[str, Any]:
|
|
if isinstance(value, dict):
|
|
return dict(value)
|
|
dump = getattr(value, "model_dump", None)
|
|
if callable(dump):
|
|
try:
|
|
return dump(mode="json", exclude_none=True)
|
|
except Exception:
|
|
return {}
|
|
return {}
|
|
|
|
|
|
def _dedupe(rows: list[str]) -> list[str]:
|
|
out: list[str] = []
|
|
seen: set[str] = set()
|
|
for row in rows:
|
|
text = str(row or "").strip()
|
|
key = " ".join(text.lower().split())
|
|
if not text or key in seen:
|
|
continue
|
|
seen.add(key)
|
|
out.append(text)
|
|
return out
|
|
|
|
|
|
def _customer_evidence_packet(*, answer: str, packet: PMFlowAgentRunPacket) -> dict[str, Any]:
|
|
return {
|
|
"packet_version": "customer_evidence_packet.v1",
|
|
"visibility": "customer",
|
|
"answer_type": "pmflow_agent_runtime",
|
|
"confidence": "high" if packet.observations else "medium",
|
|
"answer": answer,
|
|
"facts": [
|
|
fact
|
|
for observation in packet.observations[:8]
|
|
for fact in list(observation.facts or [])[:6]
|
|
if isinstance(fact, dict)
|
|
][:24],
|
|
"limitations": [
|
|
finding.get("summary")
|
|
for verification in packet.verifications
|
|
for finding in list(verification.findings or [])[:4]
|
|
if isinstance(finding, dict) and str(finding.get("summary") or "").strip()
|
|
][:12],
|
|
}
|
|
|
|
|
|
def _answer_canvas(*, answer: str, packet: PMFlowAgentRunPacket) -> dict[str, Any]:
|
|
return {
|
|
"packet_version": "answer_canvas.v1",
|
|
"visibility": "customer",
|
|
"direct_answer": answer,
|
|
"next_actions": list(packet.prepared_action_packets or [])[:6],
|
|
"confirmation_requests": list(packet.confirmation_requests or [])[:6],
|
|
"process_steps": [step.model_dump(mode="json", exclude_none=True) for step in packet.steps[:12]],
|
|
}
|
|
|
|
|
|
def _final_answer_artifact(answer: str, *, status: str | None = None) -> PMFlowAgentArtifactPacket:
|
|
return PMFlowAgentArtifactPacket(
|
|
kind="final_answer",
|
|
status=status or ("completed" if answer.strip() else "blocked"),
|
|
title="PMFlow AI 回答",
|
|
summary=answer[:1000],
|
|
payload={"answer": answer},
|
|
)
|
|
|
|
|
|
def _dedupe_steps(steps: list[PMFlowAgentStepPacket]) -> list[PMFlowAgentStepPacket]:
|
|
out: list[PMFlowAgentStepPacket] = []
|
|
seen: set[str] = set()
|
|
for step in steps:
|
|
step_id = str(step.step_id or new_pmflow_agent_id("step"))[:80]
|
|
if step_id in seen:
|
|
step_id = f"{step_id[:64]}_{len(out)}"
|
|
seen.add(step_id)
|
|
out.append(step.model_copy(update={"step_id": step_id, "index": len(out)}))
|
|
return out[:32]
|
|
|
|
|
|
def _dedupe_observations(observations: list[PMFlowAgentObservationPacket]) -> list[PMFlowAgentObservationPacket]:
|
|
out: list[PMFlowAgentObservationPacket] = []
|
|
seen: set[str] = set()
|
|
for observation in observations:
|
|
observation_id = str(observation.observation_id or new_pmflow_agent_id("obs"))[:80]
|
|
if observation_id in seen:
|
|
observation_id = f"{observation_id[:64]}_{len(out)}"
|
|
seen.add(observation_id)
|
|
out.append(observation.model_copy(update={"observation_id": observation_id}))
|
|
return out[:48]
|
|
|
|
|
|
def _dedupe_artifacts(artifacts: list[PMFlowAgentArtifactPacket]) -> list[PMFlowAgentArtifactPacket]:
|
|
out: list[PMFlowAgentArtifactPacket] = []
|
|
seen: set[str] = set()
|
|
for artifact in artifacts:
|
|
key = f"{artifact.kind}:{artifact.title}:{artifact.summary}"[:500]
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
out.append(artifact)
|
|
return out[:32]
|
|
|
|
|
|
def _dedupe_verifications(verifications: list[PMFlowAgentVerificationPacket]) -> list[PMFlowAgentVerificationPacket]:
|
|
out: list[PMFlowAgentVerificationPacket] = []
|
|
seen: set[str] = set()
|
|
for verification in verifications:
|
|
verification_id = str(verification.verification_id or new_pmflow_agent_id("ver"))[:80]
|
|
if verification_id in seen:
|
|
verification_id = f"{verification_id[:64]}_{len(out)}"
|
|
seen.add(verification_id)
|
|
out.append(verification.model_copy(update={"verification_id": verification_id}))
|
|
return out[:40]
|
|
|
|
|
|
def _dedupe_memories(memories: list[PMFlowAgentMemoryPacket]) -> list[PMFlowAgentMemoryPacket]:
|
|
out: list[PMFlowAgentMemoryPacket] = []
|
|
seen: set[str] = set()
|
|
for memory in memories:
|
|
key = f"{memory.memory_kind}:{memory.scope_ref}:{memory.summary}"[:500]
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
out.append(memory)
|
|
return out[:12]
|
|
|
|
|
|
def _title_from_question(question: Any) -> str:
|
|
text = " ".join(str(question or "").strip().split())
|
|
return text[:80] or "PMFlow AI Run"
|
|
|
|
|
|
def _customer_safe_error_summary(exc: Exception) -> str:
|
|
return f"{exc.__class__.__name__}: runtime failed"[:500]
|
|
|
|
|
|
def _safe_int(value: Any) -> int | None:
|
|
try:
|
|
if value is None or value == "":
|
|
return None
|
|
return int(value)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
__all__ = ["PMFLOW_AGENT_OS_POLICY_VERSION", "PMFlowAgentRuntimeService"]
|