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:
+41
-7
@@ -34,9 +34,9 @@ from lembas.security import permissions
|
||||
from lembas.services import audio as audio_service
|
||||
from lembas.services import chat as chat_service
|
||||
from lembas.services import compaction as compaction_service
|
||||
from lembas.services import data_groups, interaction, settings_store, sse
|
||||
from lembas.services import files as files_service
|
||||
from lembas.services import generation as generation_service
|
||||
from lembas.services import interaction, settings_store, sse
|
||||
from lembas.services import metrics as metrics_service
|
||||
from lembas.services import prompts as prompts_service
|
||||
from lembas.services import reports as reports_service
|
||||
@@ -222,6 +222,14 @@ def _new_chat(
|
||||
folder_id=folder.id if folder is not None else None,
|
||||
model_id=chosen[0] if chosen else "",
|
||||
connection_id=chosen[1] if chosen else None,
|
||||
# The chat's data group is its model's, fixed now. It is what every
|
||||
# later turn reads memories and notes from, and what decides which models
|
||||
# this chat may be switched to -- see services/data_groups.py.
|
||||
data_group_id=(
|
||||
data_groups.for_pair(db, user, chosen[0], chosen[1])
|
||||
if chosen
|
||||
else data_groups.DEFAULT_GROUP
|
||||
),
|
||||
temporary=temporary,
|
||||
kind=KIND_AGENT if profile is not None else KIND_CHAT,
|
||||
ssh_profile_id=profile.id if profile is not None else None,
|
||||
@@ -657,8 +665,12 @@ async def attach_base(
|
||||
if not permissions.has(db, user, "library.use"):
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, "You may not use the library.")
|
||||
|
||||
# Only a base in the chat's own data group: its documents are what this
|
||||
# chat's model would search, and another group's are not its to read.
|
||||
base = db.scalar(
|
||||
documents_service.visible_bases(db, user).where(KnowledgeBase.id == base_id)
|
||||
documents_service.visible_bases(db, user, data_groups.for_chat(db, chat)).where(
|
||||
KnowledgeBase.id == base_id
|
||||
)
|
||||
)
|
||||
if base is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "That knowledge base is not available.")
|
||||
@@ -1536,8 +1548,13 @@ def _apply_crowd(db: DBSession, chat: Chat, user: User, values: list[str]) -> No
|
||||
from lembas.db.models import CrowdMember
|
||||
|
||||
settings = settings_store.crowd(db)
|
||||
# Only models in the chat's own data group: a member is sent the whole
|
||||
# conversation, so one from another group would carry it to that provider.
|
||||
reachable = {
|
||||
model.model_id: model for model in chat_service.available_models(db, user)
|
||||
model.model_id: model
|
||||
for model in chat_service.available_models(
|
||||
db, user, data_groups.for_chat(db, chat)
|
||||
)
|
||||
}
|
||||
wanted: list[str] = []
|
||||
for value in values:
|
||||
@@ -2125,11 +2142,28 @@ async def update_chat(request: Request, db: Db, user: RequiredUser, chat_id: str
|
||||
)
|
||||
# Checked against what this user can reach, not merely what exists --
|
||||
# otherwise the picker is advisory and a crafted request bypasses it.
|
||||
# And within the chat's own data group: the new model would be sent the
|
||||
# whole history, which is exactly what a group keeps from its provider.
|
||||
group = data_groups.for_chat(db, chat)
|
||||
match = next(
|
||||
(m for m in chat_service.available_models(db, user) if m.model_id == model_id),
|
||||
(
|
||||
m
|
||||
for m in chat_service.available_models(db, user, group)
|
||||
if m.model_id == model_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if match is None:
|
||||
elsewhere = any(
|
||||
m.model_id == model_id for m in chat_service.available_models(db, user)
|
||||
)
|
||||
if elsewhere:
|
||||
raise HTTPException(
|
||||
status.HTTP_409_CONFLICT,
|
||||
f"That model is in another data group than this chat "
|
||||
f"({data_groups.name_of(db, group)}), so it cannot be given this "
|
||||
f"chat's history. Start a new chat with it instead.",
|
||||
)
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, "That model is not available to you.")
|
||||
chat.model_id = model_id
|
||||
chat.connection_id = match.connection_id
|
||||
@@ -2152,9 +2186,9 @@ async def update_chat(request: Request, db: Db, user: RequiredUser, chat_id: str
|
||||
chat.knowledge_bases = (
|
||||
list(
|
||||
db.scalars(
|
||||
documents_service.visible_bases(db, user).where(
|
||||
KnowledgeBase.id.in_(wanted)
|
||||
)
|
||||
documents_service.visible_bases(
|
||||
db, user, data_groups.for_chat(db, chat)
|
||||
).where(KnowledgeBase.id.in_(wanted))
|
||||
)
|
||||
)
|
||||
if wanted
|
||||
|
||||
Reference in New Issue
Block a user