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
|
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:
|
def get_llm_provider(name: str | None = None) -> LLMProvider:
|
||||||
"""The named LLM provider, or the configured default, or Null if unconfigured."""
|
"""The named LLM provider, or the configured default, or Null if unconfigured."""
|
||||||
providers = build_llm_providers()
|
providers = build_llm_providers()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from app.api.v1 import (
|
from app.api.v1 import (
|
||||||
|
ai,
|
||||||
auth,
|
auth,
|
||||||
citations,
|
citations,
|
||||||
cleanup,
|
cleanup,
|
||||||
@@ -36,3 +37,4 @@ api_router.include_router(cleanup.router)
|
|||||||
api_router.include_router(public.router)
|
api_router.include_router(public.router)
|
||||||
api_router.include_router(members.router)
|
api_router.include_router(members.router)
|
||||||
api_router.include_router(proposals.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)
|
||||||
@@ -36,6 +36,11 @@ class Tree(Base, UUIDPrimaryKey, Timestamps, SoftDelete):
|
|||||||
use_alter=True,
|
use_alter=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# Per-tree AI model policy (owner-configured). The names reference configured
|
||||||
|
# providers from the registry; null = that role has no model. The owner may
|
||||||
|
# use any configured provider; these limit members + the recommender.
|
||||||
|
ai_member_provider: Mapped[str | None] = mapped_column(String(32))
|
||||||
|
ai_recommender_provider: Mapped[str | None] = mapped_column(String(32))
|
||||||
|
|
||||||
|
|
||||||
class TreeMembership(Base, UUIDPrimaryKey, Timestamps):
|
class TreeMembership(Base, UUIDPrimaryKey, Timestamps):
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ConfiguredProvider(BaseModel):
|
||||||
|
name: str
|
||||||
|
model: str
|
||||||
|
|
||||||
|
|
||||||
|
class TreeAiPolicyRead(BaseModel):
|
||||||
|
# The model non-owners' assistant uses (null = none).
|
||||||
|
member_provider: str | None
|
||||||
|
# The model the association/recommendation engine uses (null = none).
|
||||||
|
recommender_provider: str | None
|
||||||
|
# Providers the operator has configured (from env). The owner may use any of
|
||||||
|
# these; the two settings above restrict members and the recommender to one.
|
||||||
|
configured_providers: list[ConfiguredProvider]
|
||||||
|
default_provider: str
|
||||||
|
|
||||||
|
|
||||||
|
class TreeAiPolicyUpdate(BaseModel):
|
||||||
|
member_provider: str | None = None
|
||||||
|
recommender_provider: str | None = None
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""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
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""tree AI model policy (ai_member_provider, ai_recommender_provider)
|
||||||
|
|
||||||
|
Revision ID: b2c3d4e5f6a7
|
||||||
|
Revises: a1b2c3d4e5f6
|
||||||
|
Create Date: 2026-06-09
|
||||||
|
|
||||||
|
"""
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "b2c3d4e5f6a7"
|
||||||
|
down_revision: str | None = "a1b2c3d4e5f6"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("trees", sa.Column("ai_member_provider", sa.String(length=32), nullable=True))
|
||||||
|
op.add_column("trees", sa.Column("ai_recommender_provider", sa.String(length=32), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("trees", "ai_recommender_provider")
|
||||||
|
op.drop_column("trees", "ai_member_provider")
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""Per-tree AI model policy: owner-only, validated against configured providers."""
|
||||||
|
|
||||||
|
from app.core.config import get_settings
|
||||||
|
from tests.conftest import auth, register
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ai_policy_is_owner_only(client):
|
||||||
|
owner = auth(await register(client, "ai-o@ex.com"))
|
||||||
|
editor = auth(await register(client, "ai-x@ex.com"))
|
||||||
|
tid = (await client.post("/api/v1/trees", json={"name": "T"}, headers=owner)).json()["id"]
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/trees/{tid}/members", json={"email": "ai-x@ex.com", "role": "editor"}, headers=owner
|
||||||
|
)
|
||||||
|
|
||||||
|
g = await client.get(f"/api/v1/trees/{tid}/ai", headers=owner)
|
||||||
|
assert g.status_code == 200
|
||||||
|
assert g.json()["member_provider"] is None
|
||||||
|
assert g.json()["configured_providers"] == [] # nothing configured in tests
|
||||||
|
|
||||||
|
# An editor (not owner) can neither view nor change the policy.
|
||||||
|
assert (await client.get(f"/api/v1/trees/{tid}/ai", headers=editor)).status_code == 403
|
||||||
|
assert (
|
||||||
|
await client.patch(
|
||||||
|
f"/api/v1/trees/{tid}/ai",
|
||||||
|
json={"member_provider": None, "recommender_provider": None},
|
||||||
|
headers=editor,
|
||||||
|
)
|
||||||
|
).status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ai_policy_set_and_validate(client, monkeypatch):
|
||||||
|
monkeypatch.setattr(get_settings(), "anthropic_api_key", "sk-ant-test")
|
||||||
|
owner = auth(await register(client, "ai-set@ex.com"))
|
||||||
|
tid = (await client.post("/api/v1/trees", json={"name": "T"}, headers=owner)).json()["id"]
|
||||||
|
|
||||||
|
g = (await client.get(f"/api/v1/trees/{tid}/ai", headers=owner)).json()
|
||||||
|
assert {p["name"] for p in g["configured_providers"]} == {"anthropic"}
|
||||||
|
|
||||||
|
# Assign the member + recommender model.
|
||||||
|
p = await client.patch(
|
||||||
|
f"/api/v1/trees/{tid}/ai",
|
||||||
|
json={"member_provider": "anthropic", "recommender_provider": "anthropic"},
|
||||||
|
headers=owner,
|
||||||
|
)
|
||||||
|
assert p.status_code == 200 and p.json()["member_provider"] == "anthropic"
|
||||||
|
|
||||||
|
# A provider that isn't configured is rejected.
|
||||||
|
assert (
|
||||||
|
await client.patch(
|
||||||
|
f"/api/v1/trees/{tid}/ai",
|
||||||
|
json={"member_provider": "openai", "recommender_provider": None},
|
||||||
|
headers=owner,
|
||||||
|
)
|
||||||
|
).status_code == 403
|
||||||
|
|
||||||
|
# Clearing is allowed.
|
||||||
|
c = await client.patch(
|
||||||
|
f"/api/v1/trees/{tid}/ai",
|
||||||
|
json={"member_provider": None, "recommender_provider": None},
|
||||||
|
headers=owner,
|
||||||
|
)
|
||||||
|
assert c.status_code == 200 and c.json()["member_provider"] is None
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { api } from "@/lib/api/client";
|
||||||
|
import type { components } from "@/lib/api/schema";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
|
||||||
|
type Policy = components["schemas"]["TreeAiPolicyRead"];
|
||||||
|
|
||||||
|
// `null`/"" means "no AI for this role". The <select> uses "" for that option
|
||||||
|
// and we translate to null on save.
|
||||||
|
const NONE = "";
|
||||||
|
|
||||||
|
export default function AiPolicyPage() {
|
||||||
|
const { id: treeId } = useParams<{ id: string }>();
|
||||||
|
const [policy, setPolicy] = useState<Policy | null>(null);
|
||||||
|
const [member, setMember] = useState<string>(NONE);
|
||||||
|
const [recommender, setRecommender] = useState<string>(NONE);
|
||||||
|
const [ready, setReady] = useState(false);
|
||||||
|
const [forbidden, setForbidden] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [msg, setMsg] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
const { data, response } = await api.GET("/api/v1/trees/{tree_id}/ai", {
|
||||||
|
params: { path: { tree_id: treeId } },
|
||||||
|
});
|
||||||
|
if (response.status === 403) {
|
||||||
|
setForbidden(true);
|
||||||
|
setReady(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
|
setPolicy(data);
|
||||||
|
setMember(data.member_provider ?? NONE);
|
||||||
|
setRecommender(data.recommender_provider ?? NONE);
|
||||||
|
}
|
||||||
|
setReady(true);
|
||||||
|
}, [treeId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
setSaving(true);
|
||||||
|
setMsg(null);
|
||||||
|
const { data, error } = await api.PATCH("/api/v1/trees/{tree_id}/ai", {
|
||||||
|
params: { path: { tree_id: treeId } },
|
||||||
|
body: {
|
||||||
|
member_provider: member || null,
|
||||||
|
recommender_provider: recommender || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSaving(false);
|
||||||
|
if (error || !data) {
|
||||||
|
setMsg("Couldn't save — pick a provider your operator has configured.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPolicy(data);
|
||||||
|
setMember(data.member_provider ?? NONE);
|
||||||
|
setRecommender(data.recommender_provider ?? NONE);
|
||||||
|
setMsg("Saved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
return <div className="p-6 text-sm text-[var(--muted)]">Loading…</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forbidden) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-2xl p-6">
|
||||||
|
<h1 className="text-xl font-semibold">AI models</h1>
|
||||||
|
<p className="mt-2 text-sm text-[var(--muted)]">
|
||||||
|
Only the tree owner can configure which AI models this tree uses.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const providers = policy?.configured_providers ?? [];
|
||||||
|
const dirty =
|
||||||
|
member !== (policy?.member_provider ?? NONE) ||
|
||||||
|
recommender !== (policy?.recommender_provider ?? NONE);
|
||||||
|
|
||||||
|
const Select = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
}) => (
|
||||||
|
<select
|
||||||
|
className="h-9 w-full max-w-xs rounded-md border border-[var(--border)] bg-[var(--surface)] px-2 text-sm"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value={NONE}>No AI</option>
|
||||||
|
{providers.map((p) => (
|
||||||
|
<option key={p.name} value={p.name}>
|
||||||
|
{p.name} — {p.model}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-2xl p-6">
|
||||||
|
<h1 className="text-xl font-semibold">AI models</h1>
|
||||||
|
<p className="mt-1 text-sm text-[var(--muted)]">
|
||||||
|
Choose which configured model each role uses. As the owner you can use any
|
||||||
|
configured provider; these settings pin members and the recommender to one.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{providers.length === 0 ? (
|
||||||
|
<Card className="mt-6">
|
||||||
|
<CardContent className="py-6 text-sm text-[var(--muted)]">
|
||||||
|
No AI providers are configured on this deployment. Set provider
|
||||||
|
credentials in the server environment (Anthropic, OpenAI, x.AI, or
|
||||||
|
Ollama) and they'll appear here.
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Card className="mt-6">
|
||||||
|
<CardContent className="flex flex-col gap-6 py-6">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">Members' assistant</div>
|
||||||
|
<div className="text-xs text-[var(--muted)]">
|
||||||
|
The model non-owner members' AI assistant uses.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select value={member} onChange={setMember} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">Recommender</div>
|
||||||
|
<div className="text-xs text-[var(--muted)]">
|
||||||
|
The model that finds associations and suggests connections.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select value={recommender} onChange={setRecommender} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button onClick={save} disabled={!dirty || saving}>
|
||||||
|
{saving ? "Saving…" : "Save"}
|
||||||
|
</Button>
|
||||||
|
{msg && <span className="text-sm text-[var(--muted)]">{msg}</span>}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="mt-4 text-xs text-[var(--muted)]">
|
||||||
|
<span className="font-medium">Configured providers:</span>{" "}
|
||||||
|
{providers.map((p) => `${p.name} (${p.model})`).join(", ")}.
|
||||||
|
{policy?.default_provider && (
|
||||||
|
<> Default: {policy.default_provider}.</>
|
||||||
|
)}{" "}
|
||||||
|
As the owner you can use all of them.
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Archive,
|
Archive,
|
||||||
ArrowDownUp,
|
ArrowDownUp,
|
||||||
BookText,
|
BookText,
|
||||||
|
Bot,
|
||||||
ClipboardCheck,
|
ClipboardCheck,
|
||||||
Compass,
|
Compass,
|
||||||
FolderTree,
|
FolderTree,
|
||||||
@@ -151,6 +152,12 @@ export function AppSidebar({ onNavigate }: { onNavigate?: () => void }) {
|
|||||||
icon={UserPlus}
|
icon={UserPlus}
|
||||||
active={pathname.startsWith(`/trees/${treeId}/members`)}
|
active={pathname.startsWith(`/trees/${treeId}/members`)}
|
||||||
/>
|
/>
|
||||||
|
<Item
|
||||||
|
href={`/trees/${treeId}/ai`}
|
||||||
|
label="AI models"
|
||||||
|
icon={Bot}
|
||||||
|
active={pathname.startsWith(`/trees/${treeId}/ai`)}
|
||||||
|
/>
|
||||||
<Item
|
<Item
|
||||||
href={`/trees/${treeId}/recovery`}
|
href={`/trees/${treeId}/recovery`}
|
||||||
label="Recovery"
|
label="Recovery"
|
||||||
|
|||||||
Vendored
+109
@@ -1031,6 +1031,24 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/v1/trees/{tree_id}/ai": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/** Get Ai Policy */
|
||||||
|
get: operations["get_ai_policy_api_v1_trees__tree_id__ai_get"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
/** Update Ai Policy */
|
||||||
|
patch: operations["update_ai_policy_api_v1_trees__tree_id__ai_patch"];
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export type webhooks = Record<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
@@ -1215,6 +1233,13 @@ export interface components {
|
|||||||
/** Updated */
|
/** Updated */
|
||||||
updated: number;
|
updated: number;
|
||||||
};
|
};
|
||||||
|
/** ConfiguredProvider */
|
||||||
|
ConfiguredProvider: {
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
/** Model */
|
||||||
|
model: string;
|
||||||
|
};
|
||||||
/** DeceasedApply */
|
/** DeceasedApply */
|
||||||
DeceasedApply: {
|
DeceasedApply: {
|
||||||
/** Person Ids */
|
/** Person Ids */
|
||||||
@@ -1886,6 +1911,24 @@ export interface components {
|
|||||||
/** Token */
|
/** Token */
|
||||||
token: string;
|
token: string;
|
||||||
};
|
};
|
||||||
|
/** TreeAiPolicyRead */
|
||||||
|
TreeAiPolicyRead: {
|
||||||
|
/** Member Provider */
|
||||||
|
member_provider: string | null;
|
||||||
|
/** Recommender Provider */
|
||||||
|
recommender_provider: string | null;
|
||||||
|
/** Configured Providers */
|
||||||
|
configured_providers: components["schemas"]["ConfiguredProvider"][];
|
||||||
|
/** Default Provider */
|
||||||
|
default_provider: string;
|
||||||
|
};
|
||||||
|
/** TreeAiPolicyUpdate */
|
||||||
|
TreeAiPolicyUpdate: {
|
||||||
|
/** Member Provider */
|
||||||
|
member_provider?: string | null;
|
||||||
|
/** Recommender Provider */
|
||||||
|
recommender_provider?: string | null;
|
||||||
|
};
|
||||||
/** TreeCreate */
|
/** TreeCreate */
|
||||||
TreeCreate: {
|
TreeCreate: {
|
||||||
/** Name */
|
/** Name */
|
||||||
@@ -4651,4 +4694,70 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
get_ai_policy_api_v1_trees__tree_id__ai_get: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["TreeAiPolicyRead"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
update_ai_policy_api_v1_trees__tree_id__ai_patch: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["TreeAiPolicyUpdate"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["TreeAiPolicyRead"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+186
-1
@@ -4093,6 +4093,100 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/trees/{tree_id}/ai": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"ai"
|
||||||
|
],
|
||||||
|
"summary": "Get Ai Policy",
|
||||||
|
"operationId": "get_ai_policy_api_v1_trees__tree_id__ai_get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TreeAiPolicyRead"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"tags": [
|
||||||
|
"ai"
|
||||||
|
],
|
||||||
|
"summary": "Update Ai Policy",
|
||||||
|
"operationId": "update_ai_policy_api_v1_trees__tree_id__ai_patch",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TreeAiPolicyUpdate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TreeAiPolicyRead"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
@@ -4683,6 +4777,24 @@
|
|||||||
],
|
],
|
||||||
"title": "CleanupResult"
|
"title": "CleanupResult"
|
||||||
},
|
},
|
||||||
|
"ConfiguredProvider": {
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Name"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Model"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"model"
|
||||||
|
],
|
||||||
|
"title": "ConfiguredProvider"
|
||||||
|
},
|
||||||
"DeceasedApply": {
|
"DeceasedApply": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"person_ids": {
|
"person_ids": {
|
||||||
@@ -6812,6 +6924,79 @@
|
|||||||
],
|
],
|
||||||
"title": "TokenRequest"
|
"title": "TokenRequest"
|
||||||
},
|
},
|
||||||
|
"TreeAiPolicyRead": {
|
||||||
|
"properties": {
|
||||||
|
"member_provider": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Member Provider"
|
||||||
|
},
|
||||||
|
"recommender_provider": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Recommender Provider"
|
||||||
|
},
|
||||||
|
"configured_providers": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ConfiguredProvider"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Configured Providers"
|
||||||
|
},
|
||||||
|
"default_provider": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Default Provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"member_provider",
|
||||||
|
"recommender_provider",
|
||||||
|
"configured_providers",
|
||||||
|
"default_provider"
|
||||||
|
],
|
||||||
|
"title": "TreeAiPolicyRead"
|
||||||
|
},
|
||||||
|
"TreeAiPolicyUpdate": {
|
||||||
|
"properties": {
|
||||||
|
"member_provider": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Member Provider"
|
||||||
|
},
|
||||||
|
"recommender_provider": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Recommender Provider"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"title": "TreeAiPolicyUpdate"
|
||||||
|
},
|
||||||
"TreeCreate": {
|
"TreeCreate": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
@@ -7081,4 +7266,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user