PMFlow/backend/app/services/ai_pmo/project_operating_context_b...

143 lines
5.6 KiB
Python

"""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,
)