111 lines
5.7 KiB
Python
111 lines
5.7 KiB
Python
"""ORM model for custom agents."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
from sqlalchemy import BigInteger, Boolean, 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)
|
|
# 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)
|
|
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"
|
|
__table_args__ = (UniqueConstraint("agent_id", "skill_name", name="uq_agent_skills_agent_skill"),)
|
|
|
|
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))
|