نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
"""Agent Workforce domain: employees, chat threads, tasks, runs and artifacts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
|
||||
from app.domain.base import new_id, utcnow
|
||||
from app.domain.identity.models import Role
|
||||
|
||||
|
||||
class EmployeeStatus(StrEnum):
|
||||
ACTIVE = "active"
|
||||
PAUSED = "paused"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
|
||||
class RoleKey(StrEnum):
|
||||
MANAGER = "manager"
|
||||
PLANNER = "planner"
|
||||
DEVELOPER = "developer"
|
||||
REVIEWER = "reviewer"
|
||||
TESTER = "tester"
|
||||
RESEARCHER = "researcher"
|
||||
CONTENT_CREATOR = "content_creator"
|
||||
SUPPORT = "support"
|
||||
|
||||
|
||||
class TaskStatus(StrEnum):
|
||||
NEW = "new"
|
||||
TRIAGED = "triaged"
|
||||
WAITING_APPROVAL = "waiting_approval"
|
||||
RUNNING = "running"
|
||||
DONE = "done"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class RunStatus(StrEnum):
|
||||
QUEUED = "queued"
|
||||
PLANNING = "planning"
|
||||
WAITING_APPROVAL = "waiting_approval"
|
||||
RUNNING = "running"
|
||||
TOOL_CALLING = "tool_calling"
|
||||
SANDBOX_RUNNING = "sandbox_running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
TIMED_OUT = "timed_out"
|
||||
|
||||
|
||||
TERMINAL_RUN_STATUSES: frozenset[RunStatus] = frozenset(
|
||||
{RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED, RunStatus.TIMED_OUT}
|
||||
)
|
||||
TERMINAL_TASK_STATUSES: frozenset[TaskStatus] = frozenset(
|
||||
{TaskStatus.DONE, TaskStatus.FAILED, TaskStatus.CANCELLED}
|
||||
)
|
||||
|
||||
#: Allowed run state transitions. Anything else raises in the application layer.
|
||||
RUN_TRANSITIONS: dict[RunStatus, frozenset[RunStatus]] = {
|
||||
RunStatus.QUEUED: frozenset({RunStatus.PLANNING, RunStatus.CANCELLED, RunStatus.FAILED}),
|
||||
RunStatus.PLANNING: frozenset(
|
||||
{RunStatus.RUNNING, RunStatus.WAITING_APPROVAL, RunStatus.FAILED, RunStatus.CANCELLED}
|
||||
),
|
||||
RunStatus.WAITING_APPROVAL: frozenset(
|
||||
{RunStatus.RUNNING, RunStatus.CANCELLED, RunStatus.FAILED, RunStatus.TIMED_OUT}
|
||||
),
|
||||
RunStatus.RUNNING: frozenset(
|
||||
{
|
||||
RunStatus.TOOL_CALLING,
|
||||
RunStatus.SANDBOX_RUNNING,
|
||||
RunStatus.WAITING_APPROVAL,
|
||||
RunStatus.COMPLETED,
|
||||
RunStatus.FAILED,
|
||||
RunStatus.CANCELLED,
|
||||
RunStatus.TIMED_OUT,
|
||||
}
|
||||
),
|
||||
RunStatus.TOOL_CALLING: frozenset(
|
||||
{RunStatus.RUNNING, RunStatus.WAITING_APPROVAL, RunStatus.FAILED, RunStatus.TIMED_OUT}
|
||||
),
|
||||
RunStatus.SANDBOX_RUNNING: frozenset(
|
||||
{RunStatus.RUNNING, RunStatus.FAILED, RunStatus.CANCELLED, RunStatus.TIMED_OUT}
|
||||
),
|
||||
RunStatus.COMPLETED: frozenset(),
|
||||
RunStatus.FAILED: frozenset(),
|
||||
RunStatus.CANCELLED: frozenset(),
|
||||
RunStatus.TIMED_OUT: frozenset(),
|
||||
}
|
||||
|
||||
|
||||
def can_transition_run(current: RunStatus, target: RunStatus) -> bool:
|
||||
return target in RUN_TRANSITIONS.get(current, frozenset())
|
||||
|
||||
|
||||
class MessageRole(StrEnum):
|
||||
USER = "user"
|
||||
ASSISTANT = "assistant"
|
||||
SYSTEM = "system"
|
||||
|
||||
|
||||
class ToolCallStatus(StrEnum):
|
||||
PENDING = "pending"
|
||||
AWAITING_APPROVAL = "awaiting_approval"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
SUCCEEDED = "succeeded"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class AgentTemplate:
|
||||
name: str
|
||||
role_key: RoleKey
|
||||
description: str = ""
|
||||
default_prompt: str = ""
|
||||
default_tools_json: list[str] = field(default_factory=list)
|
||||
default_routing_policy_id: str | None = None
|
||||
id: str = field(default_factory=new_id)
|
||||
is_system: bool = True
|
||||
created_at: datetime = field(default_factory=utcnow)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Employee:
|
||||
organization_id: str
|
||||
name: str
|
||||
role_key: RoleKey
|
||||
template_id: str | None = None
|
||||
project_id: str | None = None
|
||||
status: EmployeeStatus = EmployeeStatus.ACTIVE
|
||||
routing_policy_id: str | None = None
|
||||
budget_limit_json: dict = field(default_factory=dict)
|
||||
id: str = field(default_factory=new_id)
|
||||
created_at: datetime = field(default_factory=utcnow)
|
||||
|
||||
@property
|
||||
def is_available(self) -> bool:
|
||||
return self.status == EmployeeStatus.ACTIVE
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class PromptVersion:
|
||||
employee_id: str
|
||||
version: int
|
||||
content: str
|
||||
created_by_user_id: str | None = None
|
||||
id: str = field(default_factory=new_id)
|
||||
created_at: datetime = field(default_factory=utcnow)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class EmployeeToolPermission:
|
||||
employee_id: str
|
||||
tool_key: str
|
||||
allowed: bool = False
|
||||
policy_json: dict = field(default_factory=dict)
|
||||
id: str = field(default_factory=new_id)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Thread:
|
||||
organization_id: str
|
||||
title: str = ""
|
||||
project_id: str | None = None
|
||||
created_by_user_id: str | None = None
|
||||
id: str = field(default_factory=new_id)
|
||||
created_at: datetime = field(default_factory=utcnow)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Message:
|
||||
thread_id: str
|
||||
role: MessageRole
|
||||
content: str
|
||||
employee_id: str | None = None
|
||||
id: str = field(default_factory=new_id)
|
||||
created_at: datetime = field(default_factory=utcnow)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Task:
|
||||
organization_id: str
|
||||
title: str
|
||||
description: str = ""
|
||||
project_id: str | None = None
|
||||
thread_id: str | None = None
|
||||
assigned_employee_id: str | None = None
|
||||
created_by_user_id: str | None = None
|
||||
status: TaskStatus = TaskStatus.NEW
|
||||
id: str = field(default_factory=new_id)
|
||||
created_at: datetime = field(default_factory=utcnow)
|
||||
updated_at: datetime = field(default_factory=utcnow)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Run:
|
||||
task_id: str
|
||||
employee_id: str | None = None
|
||||
status: RunStatus = RunStatus.QUEUED
|
||||
error: str | None = None
|
||||
id: str = field(default_factory=new_id)
|
||||
started_at: datetime | None = None
|
||||
finished_at: datetime | None = None
|
||||
created_at: datetime = field(default_factory=utcnow)
|
||||
|
||||
@property
|
||||
def is_terminal(self) -> bool:
|
||||
return self.status in TERMINAL_RUN_STATUSES
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class RunStep:
|
||||
run_id: str
|
||||
index: int
|
||||
kind: str
|
||||
title: str = ""
|
||||
payload_json: dict = field(default_factory=dict)
|
||||
id: str = field(default_factory=new_id)
|
||||
created_at: datetime = field(default_factory=utcnow)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class ToolCall:
|
||||
run_id: str
|
||||
tool_key: str
|
||||
arguments_json: dict = field(default_factory=dict)
|
||||
result_json: dict = field(default_factory=dict)
|
||||
status: ToolCallStatus = ToolCallStatus.PENDING
|
||||
requires_approval: bool = False
|
||||
approved_by_user_id: str | None = None
|
||||
id: str = field(default_factory=new_id)
|
||||
created_at: datetime = field(default_factory=utcnow)
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class Artifact:
|
||||
organization_id: str
|
||||
name: str
|
||||
kind: str = "file"
|
||||
content_ref: str = ""
|
||||
project_id: str | None = None
|
||||
run_id: str | None = None
|
||||
size_bytes: int = 0
|
||||
id: str = field(default_factory=new_id)
|
||||
created_at: datetime = field(default_factory=utcnow)
|
||||
|
||||
|
||||
#: Roles that may only be created by an org owner/admin.
|
||||
ELEVATED_ROLE_KEYS: frozenset[RoleKey] = frozenset({RoleKey.MANAGER})
|
||||
DEFAULT_EMPLOYEE_ROLE: Role = Role.MEMBER
|
||||
@@ -0,0 +1,169 @@
|
||||
"""Tool catalog for AI employees.
|
||||
|
||||
Three rules are encoded here as data rather than as scattered ``if`` statements:
|
||||
|
||||
1. Every tool has a stable key and an explicit approval requirement.
|
||||
2. Dangerous tools require human approval **before** they run — the flag lives
|
||||
with the tool, so a new call site cannot forget it.
|
||||
3. Nothing is allowed by default; an employee needs an explicit permission row.
|
||||
|
||||
This module is pure: no I/O, no framework, no database.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class ToolKey:
|
||||
"""Stable tool identifiers. These appear in audit logs and permissions."""
|
||||
|
||||
GIT_ENSURE_REPO = "git.ensure_repo"
|
||||
GIT_CREATE_BRANCH = "git.create_branch"
|
||||
GIT_OPEN_PR = "git.open_pr"
|
||||
GIT_MERGE_PR = "git.merge_pr"
|
||||
GIT_DELETE_REPO = "git.delete_repo"
|
||||
DOMAIN_CREATE_PREVIEW = "domain.create_preview"
|
||||
DOMAIN_DELETE_PREVIEW = "domain.delete_preview"
|
||||
SANDBOX_RUN_COMMAND = "sandbox.run_command"
|
||||
SANDBOX_READ_FILE = "sandbox.read_file"
|
||||
SANDBOX_WRITE_FILE = "sandbox.write_file"
|
||||
SANDBOX_DELETE_FILE = "sandbox.delete_file"
|
||||
SANDBOX_DESTROY = "sandbox.destroy"
|
||||
KNOWLEDGE_SEARCH = "knowledge.search"
|
||||
ARTIFACT_CREATE = "artifact.create"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ToolSpec:
|
||||
key: str
|
||||
label: str
|
||||
description: str
|
||||
#: Human approval is required before this tool may execute.
|
||||
requires_approval: bool = False
|
||||
#: Irreversible or destructive: never auto-approved, always audited.
|
||||
destructive: bool = False
|
||||
#: Tools that touch the outside world are the ones worth approving.
|
||||
category: str = "general"
|
||||
|
||||
|
||||
TOOL_CATALOG: tuple[ToolSpec, ...] = (
|
||||
ToolSpec(
|
||||
key=ToolKey.GIT_ENSURE_REPO,
|
||||
label="ساخت مخزن",
|
||||
description="ساخت مخزن خصوصی Git برای پروژه.",
|
||||
category="git",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.GIT_CREATE_BRANCH,
|
||||
label="ساخت شاخه",
|
||||
description="ساخت یک شاخهٔ جدید از شاخهٔ مشخص.",
|
||||
category="git",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.GIT_OPEN_PR,
|
||||
label="باز کردن درخواست ادغام",
|
||||
description="ثبت یک PR از شاخهٔ کاری به شاخهٔ پایه.",
|
||||
category="git",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.GIT_MERGE_PR,
|
||||
label="ادغام درخواست ادغام",
|
||||
description="ادغام PR در شاخهٔ پایه. بازگشتناپذیر.",
|
||||
requires_approval=True,
|
||||
destructive=True,
|
||||
category="git",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.GIT_DELETE_REPO,
|
||||
label="حذف مخزن",
|
||||
description="حذف کامل مخزن پروژه. بازگشتناپذیر.",
|
||||
requires_approval=True,
|
||||
destructive=True,
|
||||
category="git",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.DOMAIN_CREATE_PREVIEW,
|
||||
label="ساخت دامنهٔ تست",
|
||||
description="انتشار موقت پروژه روی یک زیردامنه.",
|
||||
category="domain",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.DOMAIN_DELETE_PREVIEW,
|
||||
label="حذف دامنهٔ تست",
|
||||
description="برداشتن دامنهٔ تست از دسترس.",
|
||||
requires_approval=True,
|
||||
category="domain",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.SANDBOX_RUN_COMMAND,
|
||||
label="اجرای دستور در سندباکس",
|
||||
description="اجرای یک دستور داخل محیط ایزوله با محدودیت زمان و خروجی.",
|
||||
category="sandbox",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.SANDBOX_READ_FILE,
|
||||
label="خواندن فایل",
|
||||
description="خواندن محتوای یک فایل در سندباکس.",
|
||||
category="sandbox",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.SANDBOX_WRITE_FILE,
|
||||
label="نوشتن فایل",
|
||||
description="نوشتن یا بازنویسی یک فایل در سندباکس.",
|
||||
category="sandbox",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.SANDBOX_DELETE_FILE,
|
||||
label="حذف فایل",
|
||||
description="حذف یک فایل از سندباکس. بازگشتناپذیر.",
|
||||
requires_approval=True,
|
||||
destructive=True,
|
||||
category="sandbox",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.SANDBOX_DESTROY,
|
||||
label="نابودی سندباکس",
|
||||
description="پایان دادن به نشست سندباکس و آزادسازی منابع.",
|
||||
requires_approval=True,
|
||||
destructive=True,
|
||||
category="sandbox",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.KNOWLEDGE_SEARCH,
|
||||
label="جستوجو در دانش",
|
||||
description="جستوجو در پایگاه دانش سازمان.",
|
||||
category="knowledge",
|
||||
),
|
||||
ToolSpec(
|
||||
key=ToolKey.ARTIFACT_CREATE,
|
||||
label="ساخت خروجی",
|
||||
description="ثبت یک سند یا خروجی قابل مشاهده برای کاربر.",
|
||||
category="artifact",
|
||||
),
|
||||
)
|
||||
|
||||
_BY_KEY: dict[str, ToolSpec] = {spec.key: spec for spec in TOOL_CATALOG}
|
||||
|
||||
#: Templates that reference an unknown tool key would silently grant nothing;
|
||||
#: this set lets a test assert the seed data and the catalog agree.
|
||||
KNOWN_TOOL_KEYS: frozenset[str] = frozenset(_BY_KEY)
|
||||
|
||||
|
||||
def get_tool_spec(key: str) -> ToolSpec | None:
|
||||
return _BY_KEY.get(key)
|
||||
|
||||
|
||||
def is_known_tool(key: str) -> bool:
|
||||
return key in _BY_KEY
|
||||
|
||||
|
||||
def tool_requires_approval(key: str) -> bool:
|
||||
"""Unknown tools are treated as requiring approval, never as safe."""
|
||||
spec = _BY_KEY.get(key)
|
||||
return True if spec is None else spec.requires_approval
|
||||
|
||||
|
||||
def is_destructive(key: str) -> bool:
|
||||
spec = _BY_KEY.get(key)
|
||||
return False if spec is None else spec.destructive
|
||||
Reference in New Issue
Block a user