"""Per-tree AI model policy — owner-only. Assigns which configured provider members and the recommender use; the owner may use any configured provider. The operator decides which providers exist (env / registry); the tree owner decides who uses which. See app/api/deps.py for the registry. """ import uuid from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import configured_llm_providers from app.models.enums import MembershipRole from app.models.tree import Tree from app.models.user import User from app.services import privacy from app.services.exceptions import Forbidden async def _require_owner(session: AsyncSession, *, actor: User, tree: Tree) -> None: role = await privacy.get_membership_role(session, actor.id, tree.id) if role is not MembershipRole.owner: raise Forbidden("only the tree owner can configure AI") def _names() -> set[str]: return {p["name"] for p in configured_llm_providers()} async def get_policy(session: AsyncSession, *, actor: User, tree: Tree) -> dict: await _require_owner(session, actor=actor, tree=tree) from app.core.config import get_settings return { "member_provider": tree.ai_member_provider, "recommender_provider": tree.ai_recommender_provider, "configured_providers": configured_llm_providers(), "default_provider": get_settings().default_llm_provider, } async def update_policy( session: AsyncSession, *, actor: User, tree: Tree, member_provider: str | None, recommender_provider: str | None, ) -> dict: await _require_owner(session, actor=actor, tree=tree) valid = _names() for value in (member_provider, recommender_provider): if value is not None and value not in valid: raise Forbidden(f"'{value}' is not a configured provider") tree.ai_member_provider = member_provider tree.ai_recommender_provider = recommender_provider await session.commit() await session.refresh(tree) return await get_policy(session, actor=actor, tree=tree) # --- Resolution helpers (for the future assistant / recommender) ------------- def provider_name_for_member(tree: Tree) -> str | None: """Provider an ordinary member's assistant should use, if any.""" return tree.ai_member_provider def provider_name_for_recommender(tree: Tree) -> str | None: return tree.ai_recommender_provider def provider_name_for_owner(tree: Tree, requested: str | None = None) -> str | None: """The owner may use any configured provider; default to the requested one.""" if requested and requested in _names(): return requested return tree.ai_member_provider # fall back to the member model