"""OPC Market service.""" from __future__ import annotations from pathlib import Path from typing import Any from .context import OfficeServiceContext from .models import ServiceError, ServiceEvent, ServiceResult class MarketService: def __init__(self, context: OfficeServiceContext) -> None: self.context = context def _ensure_custom_org_editable(self) -> None: if not self.context.is_custom_org_editable(): raise ServiceError( "org_read_only", "Corporate organization is read-only. Select or create a saved custom org before editing.", ) async def browse(self) -> ServiceResult: from opc.market.architecture_registry import get_all_presets return ServiceResult({"presets": [ item.to_display_card() if hasattr(item, "to_display_card") else self._preset_payload(item) for item in get_all_presets() ]}) async def preview(self, preset_id: str) -> ServiceResult: from opc.market.architecture_registry import get_preset preset = get_preset(preset_id) if not preset: raise ServiceError("preset_not_found", "Preset not found", {"preset_id": preset_id}) return ServiceResult(preset.to_detail() if hasattr(preset, "to_detail") else {"preset": self._preset_payload(preset)}) async def apply_preset(self, *, preset_id: str, strategy: str = "overwrite") -> ServiceResult: from opc.market.architecture_registry import apply_architecture_preset_to_config, get_preset self._ensure_custom_org_editable() preset = get_preset(preset_id) if not preset: raise ServiceError("preset_not_found", f"Preset '{preset_id}' not found", {"preset_id": preset_id}) if strategy not in {"namespace", "overwrite"}: raise ServiceError("invalid_strategy", "Strategy must be namespace or overwrite") async with self.context.config_lock: info = apply_architecture_preset_to_config(self.context.engine.config, preset_id, strategy=strategy, clear_existing=True) role_ids = list(info.role_ids) work_item_template_ids = list(info.work_item_template_ids or info.template_ids) employee_ids = self._ensure_default_employees(role_ids) self._persist_config() events = await self._reload_custom_agents_for_roles(role_ids) events.extend(await self._org_events()) return ServiceResult({ "ok": True, "action": "market_preset_applied", "package_id": preset_id, "name": getattr(preset, "name", preset_id), "roles": len(role_ids), "work_item_templates": len(work_item_template_ids), "employees": len(employee_ids), }, events) async def list_installed(self) -> ServiceResult: packages = [] for package in list(getattr(self.context.engine.config.org, "installed_packages", []) or []): if hasattr(package, "model_dump"): packages.append(package.model_dump()) elif isinstance(package, dict): packages.append(package) return ServiceResult({"packages": packages}) async def export(self, *, package_id: str, name: str, description: str = "", version: str = "1.0.0", output_dir: str = ".") -> ServiceResult: from opc.market import PackageExporter if not package_id or not name: raise ServiceError("missing_package_fields", "package_id and name required") exporter = PackageExporter(self.context.engine.config, self.context.opc_home) package = exporter.export_current(package_id=package_id, name=name, description=description, version=version) out_path = exporter.write_to_path(package, Path(output_dir or self.context.opc_home / "exports")) return ServiceResult({ "ok": True, "action": "market_exported", "path": str(out_path), "package_id": package_id, "roles": len(package.roles), "templates": len(package.talent_templates), }) async def install(self, *, path: str, strategy: str = "namespace") -> ServiceResult: from opc.market import PackageLoader, SandboxChecker self._ensure_custom_org_editable() if not path: raise ServiceError("missing_path", "path required") loader = PackageLoader(self.context.engine.config, self.context.opc_home) package = loader.load_from_path(Path(path)) report = SandboxChecker().validate(package) if not report.passed: raise ServiceError("package_security_failed", "Security check failed", { "sandbox_errors": list(report.errors), "sandbox_warnings": list(report.warnings), }) async with self.context.config_lock: info = loader.install(package, strategy=strategy) employee_ids = self._ensure_default_employees(list(info.role_ids)) self._persist_config() events = await self._reload_custom_agents_for_roles(list(info.role_ids)) events.extend(await self._org_events()) return ServiceResult({ "ok": True, "action": "market_installed", "package_id": info.package_id, "name": info.name, "roles": len(info.role_ids), "templates": len(info.template_ids), "employees": len(employee_ids), "warnings": list(report.warnings), }, events) async def uninstall(self, package_id: str) -> ServiceResult: from opc.market import PackageLoader from opc.market.package_format import InstalledPackageInfo self._ensure_custom_org_editable() if not package_id: raise ServiceError("missing_package_id", "package_id required") removed_role_ids: set[str] = set() for package in self.context.engine.config.org.installed_packages: pid = package.package_id if isinstance(package, InstalledPackageInfo) else package.get("package_id", "") if pid == package_id: removed_role_ids = set(package.role_ids if isinstance(package, InstalledPackageInfo) else package.get("role_ids", [])) break async with self.context.config_lock: success = PackageLoader(self.context.engine.config, self.context.opc_home).uninstall(package_id) if success: org = getattr(self.context.engine, "org_engine", None) if org: org.reload_from_config() await self._clean_orphaned_assignments(removed_role_ids) self._persist_config() if not success: raise ServiceError("package_not_found", f"Package '{package_id}' not found", {"package_id": package_id}) events: list[ServiceEvent] = [] if removed_role_ids and self.context.agent_store is not None: try: agents = await self.context.agent_store.get_all() to_remove = [agent for agent in agents if agent.get("opc_role_id") in removed_role_ids] for agent in to_remove: await self.context.agent_store.remove_agent(agent["agent_id"]) if to_remove: if self.context.sync_role_map is not None: await self.context.sync_role_map() sync = getattr(self.context.agent_store, "sync_custom_shadow", None) if callable(sync): await sync() events.append(ServiceEvent("ack", { "ok": True, "action": "agents_spawned", "agents": await self.context.agent_store.get_all(), })) except Exception: pass events.extend(await self._org_events()) return ServiceResult({"ok": True, "action": "market_uninstalled", "package_id": package_id}, events) def _persist_config(self) -> None: org = getattr(self.context.engine, "org_engine", None) if org: org.reload_from_config() if self.context.persist_runtime_config is not None: self.context.persist_runtime_config() else: self.context.engine.config.save() def _ensure_default_employees(self, role_ids: list[str]) -> list[str]: org = getattr(self.context.engine, "org_engine", None) if not org: return [] employee_ids: list[str] = [] for role_id in role_ids: try: employee = org.ensure_default_employee_for_role(role_id, persist=False) if employee: employee_ids.append(employee.employee_id) except Exception: pass return employee_ids async def _reload_custom_agents_for_roles(self, role_ids: list[str]) -> list[ServiceEvent]: if self.context.mode_state.exec_mode not in {"org", "custom"}: return [] org = getattr(self.context.engine, "org_engine", None) store = self.context.agent_store if org is None or store is None: return [] try: db = getattr(store, "_db", None) if db is not None: await db.execute("DELETE FROM agents") await db.execute("DELETE FROM custom_agents_shadow") await db.commit() loader = getattr(store, "_load_from_role_infos", None) if callable(loader): installed = set(role_ids) roles_info = [role for role in org.list_agents() if role.role_id in installed] if roles_info: await loader(roles_info, "custom") sync = getattr(store, "sync_custom_shadow", None) if callable(sync): await sync() if self.context.sync_role_map is not None: await self.context.sync_role_map() return [ServiceEvent("ack", {"ok": True, "action": "agents_spawned", "agents": await store.get_all()})] except Exception: return [] async def _clean_orphaned_assignments(self, removed_role_ids: set[str]) -> None: if not removed_role_ids: return store = getattr(self.context.engine, "store", None) if not store: return try: tasks = await store.get_tasks(project_id=getattr(self.context.engine, "project_id", None) or "default") for task in tasks: if getattr(task, "assigned_to", "") in removed_role_ids: task.assigned_to = "" await store.save_task(task) except Exception: pass async def _org_events(self) -> list[ServiceEvent]: from .org import OrgService return (await OrgService(self.context).info(include_events=True)).events @staticmethod def _preset_payload(preset: Any) -> dict[str, Any]: return { "id": getattr(preset, "id", ""), "name": getattr(preset, "name", ""), "description": getattr(preset, "description", ""), "category": getattr(preset, "category", ""), "roles": len(getattr(preset, "roles", []) or []), "templates": len(getattr(preset, "work_item_templates", []) or []), "collaboration_pattern": getattr(preset, "collaboration_pattern", ""), } @staticmethod def _installed_payload(package: Any) -> dict[str, Any]: if hasattr(package, "package_id"): return { "package_id": package.package_id, "name": package.name, "version": package.version, "role_ids": list(package.role_ids), "template_ids": list(package.template_ids), } return dict(package or {})