mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
183 lines
7.9 KiB
Python
183 lines
7.9 KiB
Python
from types import MappingProxyType
|
|
from typing import Annotated, Final
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
|
from pydantic import AwareDatetime
|
|
|
|
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth, user_api_key_has_admin_view
|
|
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
|
from litellm.proxy.memory.memory_endpoints import require_memory_prisma
|
|
from litellm.proxy.memory.policy import (
|
|
MEMORY_CONFIG_PARAM,
|
|
MemoryIdentity,
|
|
invalidate_memory_configuration,
|
|
memory_primary_client,
|
|
memory_settings,
|
|
resolve_memory_access,
|
|
)
|
|
from litellm.proxy.memory.store import MemoryStore
|
|
from litellm.repositories.config_repository import ConfigRepository
|
|
from litellm.repositories.team_repository import TeamRepository
|
|
from litellm.repositories.user_repository import UserRepository
|
|
from litellm.types.memory_v2 import (
|
|
MemoryCapture,
|
|
MemoryEntry,
|
|
MemoryQuery,
|
|
MemorySearch,
|
|
MemorySettings,
|
|
MemorySettingsView,
|
|
MemoryStatus,
|
|
)
|
|
|
|
_AUTH: Final = Depends(user_api_key_auth)
|
|
router: Final = APIRouter(prefix="/memory/v2", tags=["memory management"]) # mutable-ok: FastAPI requires native tags.
|
|
|
|
|
|
def require_memory_admin(auth: UserAPIKeyAuth, *, write: bool = False) -> None:
|
|
if not user_api_key_has_admin_view(auth) or write and auth.user_role != LitellmUserRoles.PROXY_ADMIN:
|
|
raise HTTPException(status_code=403, detail="Only proxy administrators can configure gateway memory")
|
|
|
|
|
|
async def settings_view(settings: MemorySettings) -> MemorySettingsView:
|
|
users: Final = (
|
|
await UserRepository(memory_primary_client(require_memory_prisma())).table.find_many(
|
|
where={"user_id": {"in": list(settings.user_ids)}}, # mutable-ok: Prisma requires native JSON.
|
|
take=len(settings.user_ids),
|
|
)
|
|
if settings.user_ids
|
|
else ()
|
|
)
|
|
return MemorySettingsView(
|
|
**settings.model_dump(),
|
|
user_names=MappingProxyType(
|
|
{user.user_id: user.user_alias or user.user_email or user.user_id for user in users}
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/settings", response_model=MemorySettingsView)
|
|
async def get_settings(auth: UserAPIKeyAuth = _AUTH) -> MemorySettingsView:
|
|
require_memory_admin(auth)
|
|
return await settings_view(await memory_settings(require_memory_prisma()))
|
|
|
|
|
|
@router.put("/settings", response_model=MemorySettingsView)
|
|
async def set_settings(settings: MemorySettings, auth: UserAPIKeyAuth = _AUTH) -> MemorySettingsView:
|
|
require_memory_admin(auth, write=True)
|
|
prisma: Final = memory_primary_client(require_memory_prisma())
|
|
selected: Final = tuple(sorted(frozenset(settings.user_ids))) if not settings.everyone else ()
|
|
if settings.enabled and not settings.everyone and not selected:
|
|
raise HTTPException(status_code=422, detail="Select at least one user or enable memory for everyone")
|
|
if selected:
|
|
users: Final = await UserRepository(prisma).table.find_many(
|
|
where={"user_id": {"in": list(selected)}}, # mutable-ok: Prisma requires native query JSON.
|
|
take=len(selected),
|
|
)
|
|
if frozenset(user.user_id for user in users) != frozenset(selected):
|
|
raise HTTPException(status_code=422, detail="One or more selected users no longer exist")
|
|
saved: Final = settings.model_copy(update=MappingProxyType({"user_ids": selected}))
|
|
await ConfigRepository(prisma).set_param(MEMORY_CONFIG_PARAM, saved.model_dump(mode="json"))
|
|
await invalidate_memory_configuration()
|
|
return await settings_view(saved)
|
|
|
|
|
|
async def memory_store(auth: UserAPIKeyAuth) -> MemoryStore:
|
|
prisma: Final = memory_primary_client(require_memory_prisma())
|
|
return MemoryStore(prisma, await resolve_memory_access(prisma, MemoryIdentity.from_auth(auth)))
|
|
|
|
|
|
@router.get("/status", response_model=MemoryStatus)
|
|
async def get_status(auth: UserAPIKeyAuth = _AUTH) -> MemoryStatus:
|
|
store: Final = await memory_store(auth)
|
|
user: Final = (
|
|
await UserRepository(store.prisma_client).find_by_id(store.access.identity.user_id)
|
|
if store.access.identity.user_id
|
|
else None
|
|
)
|
|
return store.access.status.model_copy(
|
|
update=MappingProxyType({"user_name": user.user_alias or user.user_email or user.user_id if user else None})
|
|
)
|
|
|
|
|
|
async def named_entries(store: MemoryStore, entries: tuple[MemoryEntry, ...]) -> tuple[MemoryEntry, ...]:
|
|
actors: Final = tuple(frozenset(entry.actor for entry in entries if entry.actor))
|
|
teams: Final = tuple(frozenset(entry.team_id for entry in entries if entry.team_id))
|
|
users: Final = (
|
|
await UserRepository(store.prisma_client).table.find_many(
|
|
where={"user_id": {"in": list(actors)}}, # mutable-ok: Prisma requires native JSON.
|
|
take=len(actors), # mutable-ok: Prisma requires native query JSON.
|
|
)
|
|
if actors
|
|
else ()
|
|
)
|
|
team_rows: Final = (
|
|
await TeamRepository(store.prisma_client).table.find_many(
|
|
where={"team_id": {"in": list(teams)}}, # mutable-ok: Prisma requires native JSON.
|
|
take=len(teams), # mutable-ok: Prisma requires native query JSON.
|
|
)
|
|
if teams
|
|
else ()
|
|
)
|
|
names: Final = MappingProxyType(
|
|
{user.user_id: user.user_alias or user.user_email or user.user_id for user in users}
|
|
)
|
|
team_names: Final = MappingProxyType({team.team_id: team.team_alias or team.team_id for team in team_rows})
|
|
return tuple(
|
|
entry.model_copy(
|
|
update=MappingProxyType(
|
|
{
|
|
"actor_name": names.get(entry.actor or ""),
|
|
"team_name": team_names.get(entry.team_id or ""),
|
|
}
|
|
)
|
|
)
|
|
for entry in entries
|
|
)
|
|
|
|
|
|
@router.get("/entries", response_model=list[MemoryEntry])
|
|
async def list_entries(
|
|
query: Annotated[MemoryQuery, Query(max_length=500)] = "",
|
|
limit: int = Query(20, ge=1, le=20),
|
|
offset: int = Query(0, ge=0, le=10000),
|
|
before_updated_at: Annotated[AwareDatetime | None, Query()] = None,
|
|
before_memory_id: Annotated[str | None, Query(min_length=1, max_length=128)] = None,
|
|
team_id: Annotated[str | None, Query(min_length=1, max_length=256)] = None,
|
|
user_id: Annotated[str | None, Query(min_length=1, max_length=256)] = None,
|
|
auth: UserAPIKeyAuth = _AUTH,
|
|
) -> tuple[MemoryEntry, ...]:
|
|
if (before_updated_at is None) != (before_memory_id is None):
|
|
raise HTTPException(status_code=422, detail="Provide both memory cursor fields")
|
|
store: Final = await memory_store(auth)
|
|
entries: Final = await store.search(
|
|
MemorySearch(query=query, limit=limit, offset=offset),
|
|
require_active=False,
|
|
recent_first=True,
|
|
before=(before_updated_at, before_memory_id) if before_updated_at and before_memory_id else None,
|
|
team_id=team_id,
|
|
user_id=user_id,
|
|
)
|
|
return await named_entries(store, entries)
|
|
|
|
|
|
@router.get("/entries/{memory_id}", response_model=MemoryEntry)
|
|
async def read_entry(memory_id: str, auth: UserAPIKeyAuth = _AUTH) -> MemoryEntry:
|
|
store: Final = await memory_store(auth)
|
|
return (await named_entries(store, (await store.read(memory_id, require_active=False),)))[0]
|
|
|
|
|
|
@router.post("/entries", response_model=MemoryEntry)
|
|
async def capture_entry(capture: MemoryCapture, auth: UserAPIKeyAuth = _AUTH) -> MemoryEntry:
|
|
return await (await memory_store(auth)).capture(capture)
|
|
|
|
|
|
@router.put("/entries/{memory_id}", response_model=MemoryEntry)
|
|
async def update_entry(memory_id: str, capture: MemoryCapture, auth: UserAPIKeyAuth = _AUTH) -> MemoryEntry:
|
|
return await (await memory_store(auth)).update(memory_id, capture)
|
|
|
|
|
|
@router.delete("/entries/{memory_id}", status_code=204)
|
|
async def delete_entry(memory_id: str, auth: UserAPIKeyAuth = _AUTH) -> Response:
|
|
if not await (await memory_store(auth)).delete(memory_id):
|
|
raise HTTPException(status_code=404, detail="Memory not found")
|
|
return Response(status_code=204)
|