Data groups: a provider's models read only their own group's data
Every connection is in a data group. Its models are handed, and can find, only that group's memories, notes, skills, knowledge, reports and personality -- by search and by id. A chat stays in the group it was started in: switching its model, the endpoint fallback, the crowd, friends, bases and the @ menu all stay inside it, and a chat whose model has moved is refused rather than sent. A group may name its own embedder and image reviewer. data.manage lets a person make personal groups, remap connections for themselves and move their own records. Also: a search no longer mixes two embedders of the same width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ from sqlalchemy.orm import Session as DBSession
|
||||
from lembas.db.models import (
|
||||
AUTHOR_MODEL,
|
||||
AUTHOR_USER,
|
||||
DEFAULT_GROUP,
|
||||
Impression,
|
||||
Persona,
|
||||
PersonaRevision,
|
||||
@@ -54,8 +55,39 @@ MAX_VIEW_CHARS = 800
|
||||
# a model editing itself every turn cannot grow the table without limit.
|
||||
MAX_REVISIONS = 20
|
||||
|
||||
# Between a model id and a data group in a person's key. Two characters, because
|
||||
# one `@` is a character a model id could plausibly contain and this must never
|
||||
# split one.
|
||||
KEY_SEPARATOR = "@@"
|
||||
|
||||
|
||||
def key_for(model_id: str, group: str | None) -> str:
|
||||
"""The key a person's personality and impression are stored under.
|
||||
|
||||
**Namespaced by data group, and the reason is a constraint.** Both tables
|
||||
are `UNIQUE(model_key, owner_id)`, SQLite cannot alter a constraint, and
|
||||
this project's schema changes are additive only -- so a `data_group_id`
|
||||
column could not let one person hold a personality for the same model id in
|
||||
two groups, which is exactly what one model id served by two providers in
|
||||
different groups needs.
|
||||
|
||||
The default group keeps the bare model id, which is what every row written
|
||||
before groups existed already holds, so an upgrade moves nothing. The
|
||||
administrator's default (`owner_id NULL`) is always bare: it is their text,
|
||||
not a person's data, and every group falls back to it.
|
||||
"""
|
||||
if not model_id or not group or group == DEFAULT_GROUP:
|
||||
return model_id
|
||||
return f"{model_id}{KEY_SEPARATOR}{group}"
|
||||
|
||||
|
||||
def split_key(model_key: str) -> tuple[str, str]:
|
||||
"""(model id, data group) out of a stored key."""
|
||||
model_id, separator, group = (model_key or "").rpartition(KEY_SEPARATOR)
|
||||
if not separator:
|
||||
return model_key or "", DEFAULT_GROUP
|
||||
return model_id, group or DEFAULT_GROUP
|
||||
|
||||
|
||||
def get(db: DBSession, model_key: str, owner: User | None) -> Persona | None:
|
||||
"""One personality row, exactly as asked for and with no fallback.
|
||||
@@ -86,7 +118,8 @@ def effective(db: DBSession, model_key: str, owner: User | None) -> Persona | No
|
||||
own = get(db, model_key, owner)
|
||||
if own is not None:
|
||||
return own
|
||||
return get(db, model_key, None) if owner is not None else None
|
||||
# The default is keyed on the bare model id whatever group asked.
|
||||
return get(db, split_key(model_key)[0], None) if owner is not None else None
|
||||
|
||||
|
||||
def personas_of(db: DBSession, owner: User | None) -> list[Persona]:
|
||||
@@ -302,6 +335,7 @@ def view_block(db: DBSession, model_key: str, owner: User | None) -> str:
|
||||
|
||||
|
||||
__all__ = [
|
||||
"KEY_SEPARATOR",
|
||||
"MAX_PERSONA_CHARS",
|
||||
"MAX_REVISIONS",
|
||||
"MAX_VIEW_CHARS",
|
||||
@@ -312,8 +346,10 @@ __all__ = [
|
||||
"get",
|
||||
"impression",
|
||||
"impressions_for",
|
||||
"key_for",
|
||||
"personas_for",
|
||||
"personas_of",
|
||||
"split_key",
|
||||
"view_block",
|
||||
"revert",
|
||||
"write",
|
||||
|
||||
Reference in New Issue
Block a user