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:
@@ -13,7 +13,7 @@ import logging
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session as DBSession
|
||||
|
||||
from lembas.db.models import AUTHOR_MODEL, AUTHOR_USER, CHUNK_NOTE, Note, User
|
||||
from lembas.db.models import AUTHOR_MODEL, AUTHOR_USER, CHUNK_NOTE, DEFAULT_GROUP, Note, User
|
||||
from lembas.services import sharing
|
||||
from lembas.services.library import retrieval
|
||||
|
||||
@@ -26,20 +26,25 @@ MAX_BODY_CHARS = 40_000
|
||||
SNIPPET_CHARS = 800
|
||||
|
||||
|
||||
def visible(db: DBSession, user: User | None):
|
||||
return select(Note).where(sharing.visible_to(Note, user))
|
||||
def visible(db: DBSession, user: User | None, group: str | None = None):
|
||||
"""Notes this user may see; `group` narrows to one data group, for a model."""
|
||||
return select(Note).where(sharing.visible_to(Note, user, group))
|
||||
|
||||
|
||||
def get(db: DBSession, note_id: str, user: User | None) -> Note | None:
|
||||
def get(
|
||||
db: DBSession, note_id: str, user: User | None, group: str | None = None
|
||||
) -> Note | None:
|
||||
note = db.get(Note, note_id)
|
||||
if note is None or not sharing.can_read(db, note, user):
|
||||
if note is None or not sharing.can_read(db, note, user, group):
|
||||
return None
|
||||
return note
|
||||
|
||||
|
||||
def recent(db: DBSession, user: User | None, *, limit: int = 20) -> list[Note]:
|
||||
def recent(
|
||||
db: DBSession, user: User | None, *, limit: int = 20, group: str | None = None
|
||||
) -> list[Note]:
|
||||
return list(
|
||||
db.scalars(visible(db, user).order_by(Note.updated_at.desc()).limit(limit))
|
||||
db.scalars(visible(db, user, group).order_by(Note.updated_at.desc()).limit(limit))
|
||||
)
|
||||
|
||||
|
||||
@@ -50,6 +55,7 @@ def search(
|
||||
*,
|
||||
limit: int = 10,
|
||||
vector: list[float] | None = None,
|
||||
group: str | None = None,
|
||||
) -> list[Note]:
|
||||
"""Notes matching `needle` that this user may see, best match first.
|
||||
|
||||
@@ -62,16 +68,23 @@ def search(
|
||||
if not hits:
|
||||
return []
|
||||
order = {hit.id: position for position, hit in enumerate(hits)}
|
||||
rows = list(db.scalars(visible(db, user).where(Note.id.in_(list(order)))))
|
||||
rows = list(db.scalars(visible(db, user, group).where(Note.id.in_(list(order)))))
|
||||
rows.sort(key=lambda note: order.get(note.id, len(order)))
|
||||
return rows[:limit]
|
||||
|
||||
|
||||
def create(
|
||||
db: DBSession, *, owner: User, title: str, body: str, author: str = AUTHOR_USER
|
||||
db: DBSession,
|
||||
*,
|
||||
owner: User,
|
||||
title: str,
|
||||
body: str,
|
||||
author: str = AUTHOR_USER,
|
||||
group: str = DEFAULT_GROUP,
|
||||
) -> Note:
|
||||
note = Note(
|
||||
owner_id=owner.id,
|
||||
data_group_id=group or DEFAULT_GROUP,
|
||||
title=(title.strip() or "Untitled")[:MAX_TITLE_CHARS],
|
||||
body=body.strip()[:MAX_BODY_CHARS],
|
||||
author=author if author in (AUTHOR_USER, AUTHOR_MODEL) else AUTHOR_USER,
|
||||
|
||||
Reference in New Issue
Block a user