Files
ai-agent/backend/schemas/users.py
Gabriel Vidal 3d741b5b1e feat(backend): per-user notifications, guest spend on the users list, impersonation
Notifications follow the conversation's audience (users/access.audience):
whoever started it and the people it's shared with, plus the owner once they
follow a guest's conversation (writing in it, or PUT /api/conversation-follow),
minus whoever muted it (meta.follow). Pushes and asks fan out to each of them
on their own channels — the owner on the webhooks, a guest in-app + email —
and the feeds (/api/notifications, /api/notify-log, the phone's /unread)
list only what the caller hears, links rebased to the reader's host. Forms
carry forYou so "waiting for feedback" is per user.

/api/users carries spentUsd (this month), totalSpentUsd and conversations per
guest (users/runs.spend_by_user, the same sum the budget is checked against).

POST /api/users/{id}/impersonate opens a 4-hour guest session for the owner's
browser on the admin host (imp on the record: never extended, not counted in
the guest's sessions); /api/me says impersonating; logout ends it. A spawn
while impersonating skips the "guest started" ping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:16:03 +02:00

135 lines
3.7 KiB
Python

"""Guests: ``/api/me``, the owner's user admin, conversation sharing."""
from __future__ import annotations
from typing import Optional
from .base import Schema
__all__ = [
"ProjectGrant", "NotifyPrefs", "Me", "GuestUser", "UsersResponse",
"LoginLink", "ShareCandidate", "ConversationShare", "GuestShortcut",
"GuestDefaults", "GuestSkill", "GuestSkillsResponse",
]
class ProjectGrant(Schema):
slug: str
level: str # "view" | "chat" | "ship"
class NotifyPrefs(Schema):
channels: list[str] # "in_app" | "email" | "webpush"
class Me(Schema):
"""Who the browser is: the owner, or a guest with their grants."""
id: str
role: str # "owner" | "guest"
name: str
email: Optional[str] = None
readonly: bool = False
projects: list[ProjectGrant]
models: list[str]
budgetUsd: Optional[float] = None
spentUsd: Optional[float] = None
notify: Optional[NotifyPrefs] = None
mail: bool = False # outbound mail configured (login links can be mailed)
surface: Optional[str] = None # "admin" | "guest"
# A guest's starting prompt shortcuts (what Reset goes back to); None for
# the owner, whose defaults are built into the frontend.
defaultShortcuts: Optional[list["GuestShortcut"]] = None
# The owner looking through this guest's eyes (Settings → Users →
# Impersonate): the PWA shows a "back to admin" bar.
impersonating: bool = False
class GuestUser(Schema):
id: str
email: str
name: str
role: str
projects: dict[str, str]
models: list[str]
budgetUsd: float
notify: NotifyPrefs
disabled: bool
createdAt: str
lastLoginAt: Optional[str] = None
sessions: int = 0
# What their conversations cost: this month (what the budget is checked
# against) and all time, and how many conversations they started.
spentUsd: float = 0
totalSpentUsd: float = 0
conversations: int = 0
class UsersResponse(Schema):
users: list[GuestUser]
levels: list[str]
channels: list[str]
defaultModels: list[str]
mail: bool
guestUrl: Optional[str] = None
class LoginLink(Schema):
url: str
sent: bool
expiresInS: int
class ShareCandidate(Schema):
id: str
name: str
level: Optional[str] = None
class AudienceMember(Schema):
id: str # "owner" or a guest's users.json id
name: str
class ConversationShare(Schema):
sessionId: str
owner: Optional[str] = None
project: Optional[str] = None
participants: list[str]
candidates: list[ShareCandidate]
# Does the caller hear about this conversation (its pushes, asks, forms)?
notifying: bool = False
# Everyone who does — who started it, who it's shared with, the owner if
# they follow it, minus whoever muted it.
audience: list[AudienceMember] = []
class GuestShortcut(Schema):
"""A prompt shortcut in the frontend's ``PromptShortcut`` shape."""
id: str
label: str
icon: str = "Sparkles"
guideline: str = ""
defaultOn: bool = False
labelOff: Optional[str] = None
guidelineOff: Optional[str] = None
children: Optional[list[str]] = None
triggers: Optional[list[str]] = None
shared: Optional[bool] = None
class GuestDefaults(Schema):
"""What the owner sets for every guest at once (Settings → Users)."""
promptShortcuts: list[GuestShortcut]
sharedSkills: list[str] # `name` (repo) | `<project>/<name>`
class GuestSkill(Schema):
"""A skill a guest's run can use — the composer's `/` mentions."""
name: str # the invocation name (`shared:<name>` when shared)
description: str = ""
triggerWords: list[str] = []
source: str # "shared" | <project slug>
class GuestSkillsResponse(Schema):
skills: list[GuestSkill]