PMFlow/backend/app/services/pmflow_agent_runtime/environment.py

275 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Agent OS environment observation layer."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from ...schemas.pmflow_agent_runtime import (
PMFlowAgentObservationPacket,
PMFlowAgentStepPacket,
new_pmflow_agent_id,
)
from ...schemas.pmflow_ai_agent_runtime import PMFlowAIAgentObservation, PMFlowAIAgentStepProposal
from ...schemas.rag import RAGAskRequest
from ..pmflow_ai_agent_runtime.agent_tool_registry import PMFlowAIAgentToolRegistry
@dataclass(slots=True)
class PMFlowAgentEnvironmentResult:
steps: list[PMFlowAgentStepPacket] = field(default_factory=list)
observations: list[PMFlowAgentObservationPacket] = field(default_factory=list)
def customer_safe_payload(self) -> dict[str, Any]:
return {
"packet_version": "pmflow_agent_environment.v1",
"visibility": "customer",
"steps": [item.model_dump(mode="json", exclude_none=True) for item in self.steps],
"observations": [item.model_dump(mode="json", exclude_none=True) for item in self.observations],
}
class PMFlowAgentEnvironmentService:
"""Read the initial customer-safe environment for a durable Agent OS run.
This layer does not infer business intent from text. It only consumes
structured context, explicit anchors and registered read-only tools.
"""
def __init__(self, *, db: Any, tenant_id: int, tool_registry: Any | None = None) -> None:
self.db = db
self.tenant_id = int(tenant_id or 1)
self.tool_registry = tool_registry or PMFlowAIAgentToolRegistry(db=db, tenant_id=self.tenant_id)
async def observe_initial(
self,
*,
request: RAGAskRequest,
current_user: Any,
conversation_context: dict[str, Any] | None,
tenant_project_context: dict[str, Any] | None,
) -> PMFlowAgentEnvironmentResult:
result = PMFlowAgentEnvironmentResult()
snapshot_step = PMFlowAgentStepPacket(
step_id=new_pmflow_agent_id("env"),
index=0,
kind="environment_snapshot",
status="completed",
label="读取环境:建立本轮上下文快照",
summary="已记录当前空间、会话和客户安全对象锚点。",
)
result.steps.append(snapshot_step)
result.observations.append(
PMFlowAgentObservationPacket(
step_id=snapshot_step.step_id,
source_kind="environment_snapshot",
summary="已建立客户安全环境快照。",
confidence="high",
facts=_environment_facts(
request=request,
conversation_context=conversation_context,
tenant_project_context=tenant_project_context,
),
gaps=_environment_gaps(
request=request,
conversation_context=conversation_context,
tenant_project_context=tenant_project_context,
),
)
)
space_observation = await self._execute_tool(
tool_name="inspect_space_collections",
tool_input={"question": str(request.question or "").strip()[:1200]},
request=request,
current_user=current_user,
conversation_context=conversation_context,
tenant_project_context=tenant_project_context,
)
_append_tool_observation(
result,
tool_name="inspect_space_collections",
label="读取环境:检查当前空间集合",
observation=space_observation,
)
project_ref = _strong_project_ref(
request=request,
conversation_context=conversation_context,
tenant_project_context=tenant_project_context,
)
if project_ref:
project_observation = await self._execute_tool(
tool_name="inspect_project_operating_state",
tool_input={"project_ref": project_ref},
request=request,
current_user=current_user,
conversation_context=conversation_context,
tenant_project_context=tenant_project_context,
)
_append_tool_observation(
result,
tool_name="inspect_project_operating_state",
label="读取环境:检查项目世界状态",
observation=project_observation,
)
return result
async def _execute_tool(
self,
*,
tool_name: str,
tool_input: dict[str, Any],
request: RAGAskRequest,
current_user: Any,
conversation_context: dict[str, Any] | None,
tenant_project_context: dict[str, Any] | None,
) -> PMFlowAIAgentObservation:
return await self.tool_registry.execute(
proposal=PMFlowAIAgentStepProposal(
action="tool",
tool_name=tool_name,
tool_input=tool_input,
rationale="Agent OS environment observation",
expected_observation="customer-safe environment facts",
confidence="medium",
),
request=request,
workspace=None,
current_user=current_user,
conversation_context=conversation_context,
tenant_project_context=tenant_project_context,
)
def _append_tool_observation(
result: PMFlowAgentEnvironmentResult,
*,
tool_name: str,
label: str,
observation: PMFlowAIAgentObservation,
) -> None:
step = PMFlowAgentStepPacket(
step_id=new_pmflow_agent_id("env"),
index=len(result.steps),
kind="environment_tool",
status="completed",
label=label,
summary=str(getattr(observation, "summary", "") or "")[:600],
tool_name=tool_name,
)
result.steps.append(step)
result.observations.append(_packet_from_agent_observation(observation, step_id=step.step_id))
def _packet_from_agent_observation(observation: PMFlowAIAgentObservation, *, step_id: str) -> PMFlowAgentObservationPacket:
gaps = [
dict(item)
for item in list(getattr(observation, "facts", []) or [])
if isinstance(item, dict) and ("gap" in item.get("type", "") or item.get("user_actionable") is not None)
][:24]
return PMFlowAgentObservationPacket(
observation_id=str(getattr(observation, "observation_id", "") or new_pmflow_agent_id("obs"))[:80],
step_id=step_id,
source_kind=str(getattr(observation, "source", "") or "agent_tool")[:80],
summary=str(getattr(observation, "summary", "") or "")[:800],
confidence=str(getattr(observation, "confidence", "") or "unknown")[:32],
facts=[dict(item) for item in list(getattr(observation, "facts", []) or [])[:24] if isinstance(item, dict)],
gaps=gaps,
)
def _environment_facts(
*,
request: RAGAskRequest,
conversation_context: dict[str, Any] | None,
tenant_project_context: dict[str, Any] | None,
) -> list[dict[str, Any]]:
surface = _surface_context(tenant_project_context)
facts: list[dict[str, Any]] = [
{"type": "user_goal", "summary": str(request.question or "").strip()[:500]},
{"type": "surface", "name": str(surface.get("surface") or surface.get("page") or "space")[:80]},
{"type": "visible_object_count", "count": len(_visible_objects(surface))},
]
project_ref = _strong_project_ref(
request=request,
conversation_context=conversation_context,
tenant_project_context=tenant_project_context,
)
if project_ref:
facts.append({"type": "strong_object_anchor", "object_ref": project_ref, "object_type": "project"})
session_id = (conversation_context or {}).get("session_id")
if session_id is not None:
facts.append({"type": "session_anchor", "session_id": session_id})
return facts
def _environment_gaps(
*,
request: RAGAskRequest,
conversation_context: dict[str, Any] | None,
tenant_project_context: dict[str, Any] | None,
) -> list[dict[str, Any]]:
if _strong_project_ref(
request=request,
conversation_context=conversation_context,
tenant_project_context=tenant_project_context,
):
return []
return [
{
"type": "object_anchor_gap",
"summary": "本轮没有结构化强项目锚点;Agent OS 不会把可见项目列表自动当作当前项目。",
"user_actionable": False,
}
]
def _strong_project_ref(
*,
request: RAGAskRequest,
conversation_context: dict[str, Any] | None,
tenant_project_context: dict[str, Any] | None,
) -> str | None:
request_project_id = _safe_int(getattr(request, "project_id", None))
if request_project_id is not None:
return f"project:{request_project_id}"
context_project_id = _safe_int((conversation_context or {}).get("current_project_id"))
if context_project_id is not None:
return f"project:{context_project_id}"
surface = _surface_context(tenant_project_context)
focus = surface.get("current_focus_object")
if isinstance(focus, dict):
object_ref = str(focus.get("object_ref") or "").strip()
object_type = str(focus.get("object_type") or "").strip()
object_id = _safe_int(focus.get("object_id"))
if object_ref.startswith("project:"):
return object_ref[:160]
if object_type == "project" and object_id is not None:
return f"project:{object_id}"
return None
def _surface_context(tenant_project_context: dict[str, Any] | None) -> dict[str, Any]:
context = dict(tenant_project_context or {})
surface = context.get("surface_context")
return dict(surface) if isinstance(surface, dict) else {}
def _visible_objects(surface_context: dict[str, Any]) -> list[dict[str, Any]]:
rows = surface_context.get("visible_objects")
return [dict(item) for item in list(rows or []) if isinstance(item, dict)]
def _safe_int(value: Any) -> int | None:
try:
if value is None or value == "":
return None
return int(value)
except Exception:
return None
__all__ = ["PMFlowAgentEnvironmentResult", "PMFlowAgentEnvironmentService"]