Per-tree AI model policy (owner-only admin view)
The operator decides which model providers exist (env / registry — Anthropic,
OpenAI, x.AI, Ollama, several at once). The *tree owner* decides who uses which:
- Members' assistant -> one configured provider (or none)
- Recommender (association/connection finder) -> one configured provider (or none)
- Owner -> may use any configured provider
Backend: two nullable columns on `trees` (ai_member_provider,
ai_recommender_provider) + migration; `configured_llm_providers()` exposes the
registry as {name, model} with no secrets; owner-gated GET/PATCH
/trees/{id}/ai validate names against the configured set. Frontend: owner-only
"AI models" page with a dropdown per role, graceful 403 for non-owners, and a
sidebar link.
Per-model-within-a-provider selection is a follow-up; today each provider maps
to its single configured model.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
@@ -101,6 +101,22 @@ def build_llm_providers() -> dict[str, LLMProvider]:
|
||||
return providers
|
||||
|
||||
|
||||
def configured_llm_providers() -> list[dict]:
|
||||
"""Configured LLM providers as {name, model} — for the AI admin view (no
|
||||
secrets). Mirrors build_llm_providers() without constructing clients."""
|
||||
s = get_settings()
|
||||
out: list[dict] = []
|
||||
if s.anthropic_api_key:
|
||||
out.append({"name": "anthropic", "model": s.anthropic_model})
|
||||
if s.openai_api_key:
|
||||
out.append({"name": "openai", "model": s.openai_model})
|
||||
if s.xai_api_key:
|
||||
out.append({"name": "xai", "model": s.xai_model})
|
||||
if s.ollama_enabled:
|
||||
out.append({"name": "ollama", "model": s.ollama_model})
|
||||
return out
|
||||
|
||||
|
||||
def get_llm_provider(name: str | None = None) -> LLMProvider:
|
||||
"""The named LLM provider, or the configured default, or Null if unconfigured."""
|
||||
providers = build_llm_providers()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1 import (
|
||||
ai,
|
||||
auth,
|
||||
citations,
|
||||
cleanup,
|
||||
@@ -36,3 +37,4 @@ api_router.include_router(cleanup.router)
|
||||
api_router.include_router(public.router)
|
||||
api_router.include_router(members.router)
|
||||
api_router.include_router(proposals.router)
|
||||
api_router.include_router(ai.router)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Per-tree AI model policy — owner-only admin view."""
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.deps import CurrentUser, SessionDep
|
||||
from app.schemas.ai_policy import TreeAiPolicyRead, TreeAiPolicyUpdate
|
||||
from app.services import ai_policy_service, tree_service
|
||||
|
||||
router = APIRouter(prefix="/trees", tags=["ai"])
|
||||
|
||||
|
||||
@router.get("/{tree_id}/ai", response_model=TreeAiPolicyRead)
|
||||
async def get_ai_policy(
|
||||
tree_id: uuid.UUID, session: SessionDep, current: CurrentUser
|
||||
) -> TreeAiPolicyRead:
|
||||
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||
return TreeAiPolicyRead(**await ai_policy_service.get_policy(session, actor=current, tree=tree))
|
||||
|
||||
|
||||
@router.patch("/{tree_id}/ai", response_model=TreeAiPolicyRead)
|
||||
async def update_ai_policy(
|
||||
tree_id: uuid.UUID, data: TreeAiPolicyUpdate, session: SessionDep, current: CurrentUser
|
||||
) -> TreeAiPolicyRead:
|
||||
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||
policy = await ai_policy_service.update_policy(
|
||||
session,
|
||||
actor=current,
|
||||
tree=tree,
|
||||
member_provider=data.member_provider,
|
||||
recommender_provider=data.recommender_provider,
|
||||
)
|
||||
return TreeAiPolicyRead(**policy)
|
||||
Reference in New Issue
Block a user