# retoor from __future__ import annotations import json import logging from typing import Any from devplacepy.services.devii.errors import ToolInputError from .broker import InteractionBroker from .capabilities import interactions_enabled from . import prefs from .schema import sanitize_text logger = logging.getLogger("devii.interaction") class InteractionController: def __init__( self, broker: InteractionBroker, owner_kind: str = "guest", owner_id: str = "", ) -> None: self._broker = broker self._owner_kind = owner_kind self._owner_id = owner_id @property def broker(self) -> InteractionBroker: return self._broker async def dispatch(self, name: str, arguments: dict[str, Any]) -> str: if name == "interactions_get": return self._pref_get() if name == "interactions_set": return self._pref_set(arguments or {}) if name in ("ui_prompt", "ui_notify") and not interactions_enabled( self._owner_kind, self._owner_id ): raise ToolInputError( "Interactive widgets are disabled for this account. " "Enable them with interactions_set or on the profile, or use a short numbered menu." ) if name == "ui_prompt": return await self._prompt(arguments) if name == "ui_cancel": return await self._cancel(arguments) if name == "ui_notify": return await self._notify(arguments) raise ToolInputError(f"Unknown interaction tool: {name}") def _require_user(self) -> None: if self._owner_kind != "user" or not self._owner_id: raise ToolInputError( "Interactive-widget preferences are only available for signed-in users." ) def _pref_get(self) -> str: self._require_user() data = prefs.snapshot(self._owner_kind, self._owner_id) return json.dumps({"status": "success", **data}, ensure_ascii=False) def _pref_set(self, arguments: dict[str, Any]) -> str: self._require_user() reset = arguments.get("reset") if isinstance(reset, bool): do_reset = reset else: do_reset = str(reset or "").strip().lower() in ("true", "1", "yes", "on") if do_reset: data = prefs.set_user_pref(self._owner_id, None) return json.dumps({"status": "success", **data}, ensure_ascii=False) if "enabled" not in arguments or arguments.get("enabled") is None: raise ToolInputError( "Pass enabled=true/false to override, or reset=true to inherit the admin default." ) raw = arguments.get("enabled") if isinstance(raw, bool): enabled = raw else: enabled = str(raw).strip().lower() in ("true", "1", "yes", "on") data = prefs.set_user_pref(self._owner_id, enabled) return json.dumps({"status": "success", **data}, ensure_ascii=False) async def _prompt(self, arguments: dict[str, Any]) -> str: ctx = self._broker.channel_context() if "ui_prompt" not in ctx.tools and self._broker.open_id(): raise ToolInputError( "An interaction is already open. Wait for it, or call ui_cancel first." ) if "ui_prompt" not in ctx.tools: raise ToolInputError( "ui_prompt is not available. Enable interactive widgets with interactions_set " "or use a short numbered markdown menu." ) result = await self._broker.prompt(arguments or {}) return json.dumps(result.to_dict(), ensure_ascii=False) async def _cancel(self, arguments: dict[str, Any]) -> str: interaction_id = sanitize_text(arguments.get("id", ""), 64) reason = sanitize_text(arguments.get("reason", "cancelled"), 40) or "cancelled" result = await self._broker.cancel(interaction_id, reason=reason) return json.dumps(result.to_dict(), ensure_ascii=False) async def _notify(self, arguments: dict[str, Any]) -> str: text = sanitize_text(arguments.get("text", ""), 200) if not text: raise ToolInputError("'text' is required for ui_notify.") ctx = self._broker.channel_context() if not ctx.capabilities.get("ephemeral_status"): return json.dumps( { "status": "skipped", "channel_id": ctx.channel_id, "message": "ephemeral status is not supported on this channel", }, ensure_ascii=False, ) emit = getattr(self._broker, "_emit", None) if emit is not None: await emit({"type": "status", "text": text}) return json.dumps( { "status": "ok", "channel_id": ctx.channel_id, "text": text, }, ensure_ascii=False, )