128 lines
7.0 KiB
Python
128 lines
7.0 KiB
Python
"""ORM model for custom agents."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
from sqlalchemy import BigInteger, Boolean, Index, Integer, String, UniqueConstraint
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from deerflow.persistence.base import Base
|
|
from deerflow.persistence.types import BeijingDateTime, PortableJSON
|
|
|
|
|
|
class AgentRow(Base):
|
|
__tablename__ = "agents"
|
|
|
|
# Stored as a plain string so the same id can be used as the filesystem
|
|
# directory name under {base_dir}/agents/{id}/.
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(191), nullable=False, index=True)
|
|
description: Mapped[str] = mapped_column(String(1024), nullable=False, default="")
|
|
user_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
|
published: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True)
|
|
# When True, the agent is moved into the password-gated "private square"
|
|
# (私密广场) and is hidden from the regular public square / built-in list.
|
|
# The flag is independent of ``published``: a private agent is still
|
|
# visible to its owner in the "我的" tab, and visible to anyone in the
|
|
# private-square listing once the password has been verified.
|
|
is_private: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True)
|
|
# Publish square (发布广场) this agent is filed under. A pure organizational
|
|
# category with no access control — independent of ``published`` /
|
|
# ``is_private``. Empty string = "not assigned" → the listing layer treats
|
|
# it as the admin-configured default square. Each agent belongs to at most
|
|
# one square (single column), so it can never be published to two squares.
|
|
square_id: Mapped[str] = mapped_column(String(64), nullable=False, default="", server_default="", index=True)
|
|
# NULL = not featured on the homepage selector. Non-NULL = admin-curated
|
|
# featured rank (lower sorts first). Only ``published=True`` agents are
|
|
# eligible; the router enforces that invariant on writes. BIGINT so the
|
|
# frontend can seed initial ranks from ``Date.now()`` (ms-resolution
|
|
# timestamps overflow a 4-byte INT on MySQL).
|
|
featured_order: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True)
|
|
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC))
|
|
updated_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|
|
|
|
|
|
class AgentFavoriteRow(Base):
|
|
"""A user's bookmark of a (typically public-square) agent.
|
|
|
|
Lets a user collect agents from the 广场 (square) into their "我的" tab
|
|
without owning them. One row per (user, agent); the unique constraint makes
|
|
favoriting idempotent.
|
|
"""
|
|
|
|
__tablename__ = "agent_favorites"
|
|
__table_args__ = (UniqueConstraint("user_id", "agent_id", name="uq_agent_favorites_user_agent"),)
|
|
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
user_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
|
agent_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
|
# Where this favorite came from: ``"user"`` = the user bookmarked it
|
|
# themselves; ``"position"`` = it was auto-granted by the user's 岗位
|
|
# (position) via the subscription sync engine. Sync only ever adds/removes
|
|
# ``"position"`` rows, so a user's own bookmarks are never clobbered.
|
|
origin: Mapped[str] = mapped_column(String(16), nullable=False, default="user", server_default="user")
|
|
# The position that granted this favorite (only set when ``origin='position'``).
|
|
position_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
|
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC))
|
|
|
|
|
|
class AgentPinRow(Base):
|
|
"""A per-user pin that keeps an agent near the top of that user's lists."""
|
|
|
|
__tablename__ = "agent_pins"
|
|
__table_args__ = (UniqueConstraint("user_id", "agent_id", name="uq_agent_pins_user_agent"),)
|
|
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
user_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
|
agent_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
|
created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC))
|
|
|
|
|
|
class AgentSkillRow(Base):
|
|
"""Denormalized cache of a custom agent's selected skills.
|
|
|
|
``config.yaml`` under ``.deer-flow/agents/{id}/`` stays the source of truth
|
|
for the agent runtime (the lead-agent prompt reads it directly). This table
|
|
is a read cache so the gallery list endpoint can render skill badges with a
|
|
single batched ``IN`` query instead of stat+YAML-parsing every agent's
|
|
``config.yaml`` on each request. Written through on create/update and
|
|
backfilled from disk by ``_sync_legacy_agents``.
|
|
|
|
One row per (agent, skill); ``position`` preserves the configured order.
|
|
"""
|
|
|
|
__tablename__ = "agent_skills"
|
|
# Prefix-unique instead of UniqueConstraint: (64 + 120) * 4 = 736 bytes
|
|
# fits MySQL's legacy 767-byte index limit (utf8mb4); a plain unique over
|
|
# agent_id + skill_name(191) would be 1020 bytes and fail create_all with
|
|
# error 1071 there. mysql_length is ignored on SQLite/Postgres.
|
|
__table_args__ = (Index("uq_agent_skills_agent_skill", "agent_id", "skill_name", unique=True, mysql_length={"skill_name": 120}),)
|
|
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
agent_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
|
skill_name: Mapped[str] = mapped_column(String(191), nullable=False, index=True)
|
|
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
|
|
|
|
class AgentExtrasRow(Base):
|
|
"""Denormalized cache of a custom agent's ``tool_groups`` / ``model``.
|
|
|
|
Companion to :class:`AgentSkillRow` — same rationale (avoid per-agent
|
|
``config.yaml`` reads in the list path). One row per agent. The presence of
|
|
a row also marks "this agent has been mirrored": the list path treats a
|
|
missing row as "unknown / not yet backfilled" (``skills=None``) so it can
|
|
be told apart from an explicitly empty skill list.
|
|
|
|
``skills_synced_at`` records when the cache was last written from disk so
|
|
the backfill can skip agents whose ``config.yaml`` has not changed.
|
|
"""
|
|
|
|
__tablename__ = "agent_extras"
|
|
|
|
agent_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
# list[str] | None — the agent's tool_groups whitelist from config.yaml.
|
|
tool_groups: Mapped[list | None] = mapped_column(PortableJSON(), nullable=True, default=None)
|
|
model: Mapped[str | None] = mapped_column(String(191), nullable=True, default=None)
|
|
skills_synced_at: Mapped[datetime] = mapped_column(BeijingDateTime(), nullable=False, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
|