"""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"]