Rules read from the main model decide who it is offered as a crowd member, a friend and on its roster; any-to-any with denies by default, or none-to-none with allows. The crowd picker names what it holds back and why, and a model held back only by a person's own rule -- or by anything, with the new rules.override -- can still be added by hand. Another data group is now a deny that an explicit rule opens. Admin -> Model rules and a card in Settings, each with a matrix drawn by the enforcing function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
3.1 KiB
Python
86 lines
3.1 KiB
Python
"""Model rules: which model may bring which into a conversation.
|
|
|
|
The instance's layer. A person's own rules and mode are on their settings page
|
|
(`api/preferences.py`), and `services/talk.py` is where the two are combined.
|
|
|
|
The page ends in a matrix -- every main model against every other -- drawn by
|
|
`talk.matrix`, which calls the same `decide` that enforces the rules. A preview
|
|
computed any other way would be a second copy of the logic, and a preview that
|
|
disagrees with enforcement is worse than none: it is believed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Form, Request, Response, status
|
|
from fastapi.responses import RedirectResponse
|
|
|
|
from lembas.api.deps import AdminUser, Db
|
|
from lembas.api.pages import describe_verdict
|
|
from lembas.db.models import ANY_MODEL, EFFECT_ALLOW, EFFECT_DENY
|
|
from lembas.services import chat as chat_service
|
|
from lembas.services import settings_store, talk
|
|
from lembas.web.templating import render
|
|
|
|
router = APIRouter(prefix="/admin/rules", tags=["admin-rules"])
|
|
|
|
|
|
def model_ids(db, user) -> list[str]:
|
|
"""Every model id this person can reach, once each, in the admin's order."""
|
|
seen: list[str] = []
|
|
for model in chat_service.available_models(db, user):
|
|
if model.model_id not in seen:
|
|
seen.append(model.model_id)
|
|
return seen
|
|
|
|
|
|
@router.get("")
|
|
async def rules_page(request: Request, db: Db, user: AdminUser, saved: str = ""):
|
|
models = chat_service.available_models(db, user)
|
|
return render(
|
|
request,
|
|
"admin/rules.html",
|
|
{
|
|
"mode": settings_store.rules(db)["mode"],
|
|
"rules": talk.rules_of(db, None),
|
|
"model_ids": model_ids(db, user),
|
|
"any_model": ANY_MODEL,
|
|
"allow": EFFECT_ALLOW,
|
|
"deny": EFFECT_DENY,
|
|
"matrix_models": models,
|
|
# The instance's own view: no person's layer and no override, which
|
|
# is what somebody without `rules.override` gets unless they narrow.
|
|
"matrix": talk.matrix(db, None, models),
|
|
"describe_verdict": describe_verdict,
|
|
"saved": saved,
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/mode")
|
|
async def save_mode(db: Db, user: AdminUser, mode: str = Form(talk.MODE_OPEN)) -> Response:
|
|
settings_store.update(
|
|
db,
|
|
{"mode": talk.MODE_CLOSED if mode == talk.MODE_CLOSED else talk.MODE_OPEN},
|
|
key=settings_store.RULES,
|
|
)
|
|
return RedirectResponse("/admin/rules?saved=1", status_code=status.HTTP_303_SEE_OTHER)
|
|
|
|
|
|
@router.post("")
|
|
async def add_rule(
|
|
db: Db,
|
|
user: AdminUser,
|
|
from_model: str = Form(ANY_MODEL),
|
|
to_model: str = Form(ANY_MODEL),
|
|
effect: str = Form(EFFECT_DENY),
|
|
both: bool = Form(False),
|
|
) -> Response:
|
|
talk.set_rule(db, None, from_model, to_model, effect, both=both)
|
|
return RedirectResponse("/admin/rules?saved=1", status_code=status.HTTP_303_SEE_OTHER)
|
|
|
|
|
|
@router.post("/{rule_id}/delete")
|
|
async def delete_rule(db: Db, user: AdminUser, rule_id: str) -> Response:
|
|
talk.delete_rule(db, None, rule_id)
|
|
return RedirectResponse("/admin/rules?saved=1", status_code=status.HTTP_303_SEE_OTHER)
|