"""Build a minimal unified project operating context packet.""" from __future__ import annotations from pathlib import Path from typing import Any, Optional from app.schemas.project_operating_context import ProjectOperatingContext def _safe_int(value: Any) -> Optional[int]: try: if value is None: return None return int(value) except Exception: return None class ProjectOperatingContextBuilder: def __init__(self, repo_scope: str | None = None) -> None: self.repo_scope = str(repo_scope or "").strip() or self._default_repo_scope() @staticmethod def _default_repo_scope() -> str: return Path(__file__).resolve().parents[4].name @staticmethod def _normalize_anchor(value: Any) -> dict[str, Any]: if not isinstance(value, dict): return {} normalized: dict[str, Any] = {} for key, raw in value.items(): if not str(key or "").strip(): continue if raw in (None, "", [], {}): continue normalized[str(key).strip()] = raw return normalized @staticmethod def _project_id_from_route_plan(value: Any) -> Optional[int]: plan = value if isinstance(value, dict) else {} internal_plan = plan.get("internal_plan") if isinstance(plan.get("internal_plan"), dict) else {} for raw in list(internal_plan.get("objects") or []): text = str(raw or "").strip().lower() if not text.startswith("project:"): continue project_id = _safe_int(text.split(":", 1)[1]) if project_id is not None: return project_id return _safe_int(plan.get("project_binding")) @staticmethod def _project_id_from_workspace(value: Any) -> Optional[int]: workspace = value if isinstance(value, dict) else {} object_ref = str(workspace.get("primary_object_ref") or "").strip().lower() if object_ref.startswith("project:"): return _safe_int(object_ref.split(":", 1)[1]) return _safe_int(workspace.get("primary_object_id")) def build( self, *, current_project_id: int | None = None, page_anchor: dict[str, Any] | None = None, thread_anchor: dict[str, Any] | None = None, ) -> ProjectOperatingContext: return ProjectOperatingContext( current_project_id=_safe_int(current_project_id), current_repo_scope=self.repo_scope, page_anchor=self._normalize_anchor(page_anchor), thread_anchor=self._normalize_anchor(thread_anchor), ) def build_from_ingress( self, *, project_hint_id: int | None = None, rag_request_context: dict[str, Any] | None = None, thread_context: dict[str, Any] | None = None, world_model: dict[str, Any] | None = None, session_project_id: int | None = None, existing_packet: dict[str, Any] | None = None, ) -> ProjectOperatingContext: context = rag_request_context if isinstance(rag_request_context, dict) else {} world = world_model if isinstance(world_model, dict) else {} existing = existing_packet if isinstance(existing_packet, dict) else {} route_plan = context.get("pmflow_ai_route_plan") if isinstance(context.get("pmflow_ai_route_plan"), dict) else {} active_workspace = ( context.get("active_workspace") if isinstance(context.get("active_workspace"), dict) else existing.get("active_workspace") if isinstance(existing.get("active_workspace"), dict) else {} ) page_anchor = self._normalize_anchor(existing.get("page_anchor")) if not page_anchor: requested_project_id = ( _safe_int(context.get("project_id")) or _safe_int(project_hint_id) or self._project_id_from_workspace(active_workspace) or self._project_id_from_route_plan(route_plan) ) source_chat_id = str(context.get("source_chat_id") or "").strip() if requested_project_id is not None: page_anchor["project_id"] = requested_project_id if source_chat_id: page_anchor["source_chat_id"] = source_chat_id thread_anchor = self._normalize_anchor(existing.get("thread_anchor")) if not thread_anchor: raw_thread = thread_context if isinstance(thread_context, dict) else {} reply_to_message_id = str(raw_thread.get("reply_to_message_id") or "").strip() root_message_id = str(raw_thread.get("root_message_id") or "").strip() if reply_to_message_id: thread_anchor["reply_to_message_id"] = reply_to_message_id if root_message_id: thread_anchor["root_message_id"] = root_message_id current_project_id = ( _safe_int(world.get("resolved_project_id")) or _safe_int(existing.get("current_project_id")) or _safe_int(session_project_id) or self._project_id_from_workspace(active_workspace) or self._project_id_from_route_plan(route_plan) or _safe_int(context.get("project_id")) or _safe_int(project_hint_id) ) repo_scope = str( world.get("repo_scope") or existing.get("current_repo_scope") or self.repo_scope or "" ).strip() or self.repo_scope return ProjectOperatingContextBuilder(repo_scope=repo_scope).build( current_project_id=current_project_id, page_anchor=page_anchor, thread_anchor=thread_anchor, )