232 lines
8.6 KiB
Python
232 lines
8.6 KiB
Python
"""Abstract persistence API for custom agents."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from typing import Any
|
|
|
|
|
|
class AgentStore(ABC):
|
|
@abstractmethod
|
|
async def list_visible(self, user_id: str) -> list[dict[str, Any]]:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def get_visible(self, agent_id: str, user_id: str) -> dict[str, Any] | None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def get_any(self, agent_id: str) -> dict[str, Any] | None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def get_owned(self, agent_id: str, user_id: str) -> dict[str, Any] | None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def create(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def ensure_builtin(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def update(self, agent_id: str, user_id: str, data: dict[str, Any]) -> dict[str, Any] | None:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def update_builtin(self, agent_id: str, data: dict[str, Any]) -> dict[str, Any] | None:
|
|
"""Update a built-in agent (user_id IS NULL).
|
|
|
|
Caller is responsible for authorization — this method does NOT check
|
|
any user identity and returns ``None`` if the target is not a built-in.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def delete(self, agent_id: str, user_id: str) -> bool:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def delete_builtin(self, agent_id: str) -> bool:
|
|
"""Delete a built-in agent (user_id IS NULL).
|
|
|
|
Caller is responsible for authorization — this method does NOT check
|
|
any user identity and returns ``False`` if the target is not a built-in.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def count_owned(self, user_id: str) -> int:
|
|
"""Return the number of agents owned by ``user_id`` (user_id IS NOT NULL)."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def set_featured(self, agent_id: str, order: int | None) -> dict[str, Any] | None:
|
|
"""Set or clear the homepage-selector featured rank for an agent.
|
|
|
|
Returns the updated row, or ``None`` if the agent does not exist.
|
|
Caller is responsible for authorization (admin) and for verifying
|
|
that ``published`` is true when ``order`` is not ``None``.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def list_featured(self, user_id: str) -> list[dict[str, Any]]:
|
|
"""Return featured + still-visible-to-``user_id`` agents, ordered by ``featured_order`` asc."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def list_by_ids(self, agent_ids: list[str], user_id: str) -> list[dict[str, Any]]:
|
|
"""Return rows for ``agent_ids`` visible to ``user_id``, preserving the input order."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def list_private(self) -> list[dict[str, Any]]:
|
|
"""Return every agent flagged ``is_private=True``.
|
|
|
|
Caller is responsible for authorization — typically the caller has
|
|
already verified the admin-configured private-square password.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def list_all(self) -> list[dict[str, Any]]:
|
|
"""Return every agent regardless of ownership/visibility.
|
|
|
|
Caller is responsible for authorization — typically the caller has
|
|
already verified ``system_role == "admin"``.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def update_any(self, agent_id: str, data: dict[str, Any]) -> dict[str, Any] | None:
|
|
"""Update any agent row by id, regardless of ownership.
|
|
|
|
Caller is responsible for authorization (admin only). Returns ``None``
|
|
if the agent does not exist.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def delete_any(self, agent_id: str) -> bool:
|
|
"""Delete any agent row by id, regardless of ownership.
|
|
|
|
Caller is responsible for authorization (admin only). Returns
|
|
``False`` if the agent does not exist.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def list_paginated(
|
|
self,
|
|
*,
|
|
user_id: str,
|
|
scope: str | None,
|
|
search: str | None,
|
|
tag_ids: list[str] | None,
|
|
page: int,
|
|
page_size: int,
|
|
admin: bool = False,
|
|
sort: str = "time_desc",
|
|
owner: str | None = None,
|
|
square_id: str | None = None,
|
|
square_is_default: bool = False,
|
|
) -> tuple[list[dict[str, Any]], int]:
|
|
"""Return one page of agents plus the total matching count.
|
|
|
|
``scope``:
|
|
- ``"mine"`` — owned by ``user_id`` OR favorited by ``user_id``.
|
|
- ``"square"`` — built-in or published, and not private-square.
|
|
- ``"private"``— flagged ``is_private=True`` (caller gates access).
|
|
- ``None`` — the default listing visibility (``list_visible``).
|
|
``admin=True`` bypasses the visibility/scope filter entirely (every
|
|
agent), but still honors ``search`` / ``tag_ids``. Caller authorizes.
|
|
|
|
``search`` matches the raw name OR its desensitized display form.
|
|
``tag_ids`` keeps agents carrying ANY of the tags (OR). ``owner`` keeps
|
|
only agents published by that user (``user_id`` equality) — built-in
|
|
agents (no owner) never match. All filters, the COUNT, and LIMIT/OFFSET
|
|
all execute in SQL.
|
|
|
|
Returns ``(rows, total)`` where ``rows`` is at most ``page_size`` items
|
|
for the 1-based ``page``.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
# --- Listing cache (skills / tool_groups / model) ---
|
|
|
|
@abstractmethod
|
|
async def replace_skills(self, agent_id: str, skills: list[str] | None) -> None:
|
|
"""Replace the cached skill list for ``agent_id``.
|
|
|
|
``None`` and ``[]`` both clear the cache to "no skills" (mirrors the
|
|
tri-state semantics of ``AgentConfig.skills``).
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def set_extras(self, agent_id: str, *, tool_groups: list[str] | None, model: str | None) -> None:
|
|
"""Upsert the cached ``tool_groups`` / ``model`` for ``agent_id``."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def get_extras_for(self, agent_ids: list[str]) -> dict[str, dict[str, Any]]:
|
|
"""Batch-fetch cached listing extras for ``agent_ids``.
|
|
|
|
Returns ``{agent_id: {"skills": list[str] | None, "tool_groups":
|
|
list[str] | None, "model": str | None}}``. An agent that was never
|
|
mirrored is absent from the mapping (caller treats it as unknown).
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def delete_extras_for_agent(self, agent_id: str) -> None:
|
|
"""Drop the cached skills + extras rows for ``agent_id`` (on delete)."""
|
|
raise NotImplementedError
|
|
|
|
# --- Favorites ("收藏") ---
|
|
|
|
@abstractmethod
|
|
async def add_favorite(self, user_id: str, agent_id: str) -> None:
|
|
"""Bookmark ``agent_id`` for ``user_id``. Idempotent."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def remove_favorite(self, user_id: str, agent_id: str) -> bool:
|
|
"""Remove a bookmark. Returns ``False`` when none existed."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def list_favorite_ids(self, user_id: str) -> set[str]:
|
|
"""Return the set of agent ids ``user_id`` has favorited."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def delete_favorites_for_agent(self, agent_id: str) -> None:
|
|
"""Drop every user's favorite of ``agent_id`` (called on agent delete)."""
|
|
raise NotImplementedError
|
|
|
|
# --- Per-user pins ("置顶") ---
|
|
|
|
@abstractmethod
|
|
async def add_pin(self, user_id: str, agent_id: str) -> None:
|
|
"""Pin ``agent_id`` for ``user_id``. Idempotent."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def remove_pin(self, user_id: str, agent_id: str) -> bool:
|
|
"""Remove a per-user pin. Returns ``False`` when none existed."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def list_pin_ids(self, user_id: str) -> set[str]:
|
|
"""Return the set of agent ids pinned by ``user_id``."""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
async def delete_pins_for_agent(self, agent_id: str) -> None:
|
|
"""Drop every user's pin of ``agent_id`` (called on agent delete)."""
|
|
raise NotImplementedError
|