deerflow-code/offline-backend-20260512/backend/packages/harness/deerflow/persistence/positions/model.py
2026-09-07 18:24:55 +08:00

74 lines
3.5 KiB
Python

"""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))