275 lines
10 KiB
Python
275 lines
10 KiB
Python
"""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"]
|