"""ORM models for 岗位 (positions) and their per-resource tag bindings. A **position** (岗位) is an admin-managed aggregation layer that decides which agents / skills / scheduled tasks / recommended questions a user is subscribed to. Each user has at most one position (the link lives on ``users.position_id``, a 1:1 relation — see ``UserRow``). Two tables here: - ``positions`` — the position itself. ``is_default`` marks the fallback position inherited by users who have not been assigned one explicitly. - ``position_tag_bindings`` — for each ``(position, resource_type)`` the set of tags whose resources are granted to the position's members. **No binding for a resource_type means "use the default set"** (see ``deerflow.services. position_sync`` for what "default" resolves to per type). """ from __future__ import annotations from datetime import UTC, datetime from sqlalchemy import Boolean, Index, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from deerflow.persistence.base import Base from deerflow.persistence.types import BeijingDateTime # Resource types a position can grant. The first three reuse the existing # typed-tag namespace (``deerflow.persistence.tags``); ``recommended_question`` # was added alongside (see TAG_TYPES). The removed ``user_prompt`` type is no # longer accepted — ``get_tag_bindings`` drops any stale rows of that type. POSITION_RESOURCE_TYPES: tuple[str, ...] = ( "agent", "skill", "scheduled_task", "recommended_question", ) class PositionRow(Base): __tablename__ = "positions" __table_args__ = (UniqueConstraint("name", name="uq_positions_name"),) id: Mapped[str] = mapped_column(String(64), primary_key=True) name: Mapped[str] = mapped_column(String(191), nullable=False) description: Mapped[str] = mapped_column(String(1024), nullable=False, default="") # Exactly one position should carry ``is_default=True`` at a time (the # router enforces "only one default" by clearing the flag on others). Users # with no explicit ``users.position_id`` inherit this position's grants. is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, 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 PositionTagBindingRow(Base): """One bound tag for a ``(position, resource_type)`` pair. The absence of *any* row for a ``(position, resource_type)`` means the position uses the **default set** for that resource type. One or more rows mean the position grants the *union* of the resources carrying those tags. """ __tablename__ = "position_tag_bindings" __table_args__ = ( UniqueConstraint("position_id", "resource_type", "tag_id", name="uq_position_tag_bindings"), Index("ix_position_tag_bindings_position_type", "position_id", "resource_type"), ) id: Mapped[str] = mapped_column(String(64), primary_key=True) position_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True) resource_type: Mapped[str] = mapped_column(String(32), nullable=False, index=True) tag_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))