174 lines
8.2 KiB
Python
174 lines
8.2 KiB
Python
"""Digital Employee (Agent) models."""
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, Text, func
|
|
from sqlalchemy.dialects.postgresql import JSON, UUID
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.database import Base
|
|
|
|
# Default context window size — used as the fallback when
|
|
# agent.context_window_size is None or 0 across all channels.
|
|
# Centralizing this constant prevents inconsistent fallback values
|
|
# (see: https://github.com/dataelement/Clawith/issues/238).
|
|
DEFAULT_CONTEXT_WINDOW_SIZE = 100
|
|
|
|
|
|
class Agent(Base):
|
|
"""Digital employee (Agent) instance.
|
|
|
|
agent_type: 'native' (platform-hosted) or 'openclaw' (remote OpenClaw bot).
|
|
"""
|
|
|
|
__tablename__ = "agents"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
avatar_url: Mapped[str | None] = mapped_column(String(500))
|
|
role_description: Mapped[str] = mapped_column(String(500), default="")
|
|
bio: Mapped[str | None] = mapped_column(Text)
|
|
welcome_message: Mapped[str | None] = mapped_column(Text, default=None)
|
|
|
|
# Ownership
|
|
creator_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
|
|
tenant_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id"))
|
|
|
|
# Agent type: 'native' (platform-hosted LLM) or 'openclaw' (remote OpenClaw bot)
|
|
agent_type: Mapped[str] = mapped_column(String(20), default="native", nullable=False)
|
|
# API key hash for OpenClaw gateway authentication
|
|
api_key_hash: Mapped[str | None] = mapped_column(String(128))
|
|
# Last time OpenClaw polled the gateway (online status indicator)
|
|
openclaw_last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
# Runtime
|
|
status: Mapped[str] = mapped_column(
|
|
Enum("creating", "running", "idle", "stopped", "error", name="agent_status_enum", create_constraint=False),
|
|
default="creating",
|
|
nullable=False,
|
|
)
|
|
container_id: Mapped[str | None] = mapped_column(String(100))
|
|
container_port: Mapped[int | None] = mapped_column(Integer)
|
|
|
|
# LLM config
|
|
primary_model_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("llm_models.id"))
|
|
fallback_model_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("llm_models.id"))
|
|
|
|
# Autonomy policy (L1/L2/L3)
|
|
autonomy_policy: Mapped[dict] = mapped_column(
|
|
JSON,
|
|
default={
|
|
"read_files": "L1",
|
|
"write_workspace_files": "L2",
|
|
"send_feishu_message": "L2",
|
|
"send_external_message": "L3",
|
|
"modify_soul": "L3",
|
|
"access_business_system_read": "L2",
|
|
"access_business_system_write": "L3",
|
|
"delete_files": "L3",
|
|
"create_calendar_event": "L2",
|
|
"financial_operations": "L3",
|
|
},
|
|
)
|
|
|
|
# Token usage control
|
|
max_tokens_per_day: Mapped[int | None] = mapped_column(Integer)
|
|
max_tokens_per_month: Mapped[int | None] = mapped_column(Integer)
|
|
tokens_used_today: Mapped[int] = mapped_column(Integer, default=0)
|
|
tokens_used_month: Mapped[int] = mapped_column(Integer, default=0)
|
|
last_daily_reset: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
last_monthly_reset: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
tokens_used_total: Mapped[int] = mapped_column(Integer, default=0)
|
|
context_window_size: Mapped[int] = mapped_column(Integer, default=100)
|
|
max_tool_rounds: Mapped[int] = mapped_column(Integer, default=50)
|
|
|
|
# Trigger limits (per-agent, configurable from Settings UI)
|
|
max_triggers: Mapped[int] = mapped_column(Integer, default=20)
|
|
min_poll_interval_min: Mapped[int] = mapped_column(Integer, default=5)
|
|
webhook_rate_limit: Mapped[int] = mapped_column(Integer, default=5)
|
|
|
|
# Expiry control
|
|
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
is_expired: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
|
|
# Daily LLM call limit
|
|
llm_calls_today: Mapped[int] = mapped_column(Integer, default=0)
|
|
max_llm_calls_per_day: Mapped[int] = mapped_column(Integer, default=100)
|
|
llm_calls_reset_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
# Template
|
|
template_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("agent_templates.id"))
|
|
|
|
# Heartbeat (proactive agent awareness)
|
|
heartbeat_enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
heartbeat_interval_minutes: Mapped[int] = mapped_column(Integer, default=240)
|
|
heartbeat_active_hours: Mapped[str] = mapped_column(String(20), default="09:00-18:00")
|
|
last_heartbeat_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
# Timezone (IANA format, e.g. "Asia/Shanghai"). None = inherit from tenant.
|
|
timezone: Mapped[str | None] = mapped_column(String(50), default=None, nullable=True)
|
|
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
|
)
|
|
last_active_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
|
|
# Relationships
|
|
creator: Mapped["User"] = relationship("User", back_populates="created_agents", foreign_keys=[creator_id])
|
|
|
|
@property
|
|
def has_api_key(self) -> bool:
|
|
"""Whether this agent has an API key configured."""
|
|
return bool(self.api_key_hash)
|
|
permissions: Mapped[list["AgentPermission"]] = relationship(back_populates="agent", cascade="all, delete-orphan")
|
|
tasks: Mapped[list["Task"]] = relationship(back_populates="agent", cascade="all, delete-orphan")
|
|
channel_config: Mapped["ChannelConfig | None"] = relationship(back_populates="agent", uselist=False)
|
|
primary_model: Mapped["LLMModel | None"] = relationship(foreign_keys=[primary_model_id])
|
|
fallback_model: Mapped["LLMModel | None"] = relationship(foreign_keys=[fallback_model_id])
|
|
|
|
|
|
class AgentPermission(Base):
|
|
"""Access permission for a digital employee."""
|
|
|
|
__tablename__ = "agent_permissions"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
agent_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("agents.id"), nullable=False)
|
|
scope_type: Mapped[str] = mapped_column(
|
|
Enum("company", "department", "user", name="permission_scope_enum"),
|
|
nullable=False,
|
|
)
|
|
# scope_id: null for company, user_id for user scope
|
|
scope_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
|
|
# access_level: 'use' = task/chat/tool/skill/workspace only, 'manage' = full access
|
|
access_level: Mapped[str] = mapped_column(String(20), default="use", nullable=False)
|
|
|
|
agent: Mapped["Agent"] = relationship(back_populates="permissions")
|
|
|
|
|
|
class AgentTemplate(Base):
|
|
"""Digital employee template for quick creation."""
|
|
|
|
__tablename__ = "agent_templates"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
description: Mapped[str] = mapped_column(Text, default="")
|
|
icon: Mapped[str] = mapped_column(String(50), default="🤖")
|
|
category: Mapped[str] = mapped_column(String(50), default="general")
|
|
soul_template: Mapped[str] = mapped_column(Text, default="")
|
|
default_skills: Mapped[list] = mapped_column(JSON, default=[])
|
|
default_autonomy_policy: Mapped[dict] = mapped_column(JSON, default={})
|
|
is_builtin: Mapped[bool] = mapped_column(default=False)
|
|
created_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"))
|
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
|
|
# Import for relationship resolution
|
|
from app.models.task import Task # noqa: E402, F401
|
|
from app.models.channel_config import ChannelConfig # noqa: E402, F401
|
|
from app.models.user import User # noqa: E402, F401
|
|
from app.models.llm import LLMModel # noqa: E402, F401
|