This commit is contained in:
2026-03-17 18:32:44 +03:00
commit efcd4a8dfd
209 changed files with 33355 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
from app.models.base import Base
from app.models.user import User, UserRole
from app.models.action import Action, ActionIngestStatus, HttpMethod
from app.models.capability import Capability
from app.models.execution import (
ExecutionRun,
ExecutionRunStatus,
ExecutionStepRun,
ExecutionStepStatus,
)
from app.models.pipeline import Pipeline, PipelineStatus
from app.models.pipeline_dialog import (
DialogMessageRole,
PipelineDialog,
PipelineDialogMessage,
)
__all__ = [
"Base",
"User",
"UserRole",
"Action",
"ActionIngestStatus",
"HttpMethod",
"Capability",
"ExecutionRun",
"ExecutionRunStatus",
"ExecutionStepRun",
"ExecutionStepStatus",
"Pipeline",
"PipelineStatus",
"DialogMessageRole",
"PipelineDialog",
"PipelineDialogMessage",
]
+115
View File
@@ -0,0 +1,115 @@
from __future__ import annotations
import enum
import uuid
from typing import Any
from sqlalchemy import Boolean, Enum, ForeignKey, Index, String, Text
from sqlalchemy.dialects.postgresql import JSON, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin
class HttpMethod(str, enum.Enum):
GET = "GET"
POST = "POST"
PUT = "PUT"
PATCH = "PATCH"
DELETE = "DELETE"
HEAD = "HEAD"
OPTIONS = "OPTIONS"
class ActionIngestStatus(str, enum.Enum):
SUCCEEDED = "SUCCEEDED"
FAILED = "FAILED"
class Action(TimestampMixin, Base):
__tablename__ = "actions"
__table_args__ = (
Index("ix_actions_method_path", "method", "path"),
)
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
user_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=True,
index=True,
comment="Owner of imported action",
)
operation_id: Mapped[str | None] = mapped_column(
String(255),
nullable=True,
index=True,
)
method: Mapped[HttpMethod] = mapped_column(
Enum(HttpMethod, name="http_method"),
nullable=False,
)
path: Mapped[str] = mapped_column(
String(2048),
nullable=False,
)
base_url: Mapped[str | None] = mapped_column(
String(2048),
nullable=True,
)
summary: Mapped[str | None] = mapped_column(
String(512),
nullable=True,
)
description: Mapped[str | None] = mapped_column(
Text,
nullable=True,
)
tags: Mapped[list[str] | None] = mapped_column(
JSON,
nullable=True,
)
parameters_schema: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
request_body_schema: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
response_schema: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
source_filename: Mapped[str | None] = mapped_column(
String(512),
nullable=True,
)
raw_spec: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
ingest_status: Mapped[ActionIngestStatus] = mapped_column(
Enum(ActionIngestStatus, name="action_ingest_status", native_enum=False),
nullable=False,
default=ActionIngestStatus.SUCCEEDED,
server_default=ActionIngestStatus.SUCCEEDED.value,
index=True,
)
ingest_error: Mapped[str | None] = mapped_column(
Text,
nullable=True,
)
is_deleted: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=False,
server_default="false",
index=True,
)
owner = relationship("User", lazy="select")
+21
View File
@@ -0,0 +1,21 @@
from datetime import datetime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import DateTime, func
class Base(DeclarativeBase):
pass
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
+86
View File
@@ -0,0 +1,86 @@
from __future__ import annotations
import enum
import uuid
from typing import Any
from sqlalchemy import Enum, ForeignKey, Index, String, Text, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSON, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin
class CapabilityType(str, enum.Enum):
ATOMIC = "ATOMIC"
COMPOSITE = "COMPOSITE"
class Capability(TimestampMixin, Base):
__tablename__ = "capabilities"
__table_args__ = (
Index("ix_capabilities_action_id", "action_id"),
UniqueConstraint(
"user_id",
"action_id",
name="uq_capabilities_user_action",
),
)
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
user_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=True,
index=True,
comment="Owner of capability",
)
action_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("actions.id", ondelete="CASCADE"),
nullable=True,
comment="Action source for atomic capability",
)
type: Mapped[CapabilityType] = mapped_column(
Enum(CapabilityType, name="capability_type", native_enum=False),
nullable=False,
default=CapabilityType.ATOMIC,
server_default=CapabilityType.ATOMIC.value,
index=True,
)
name: Mapped[str] = mapped_column(
String(255),
nullable=False,
index=True,
)
description: Mapped[str | None] = mapped_column(
Text,
nullable=True,
)
input_schema: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
output_schema: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
recipe: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
data_format: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
llm_payload: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
action = relationship("Action", lazy="select")
owner = relationship("User", lazy="select")
+159
View File
@@ -0,0 +1,159 @@
from __future__ import annotations
import enum
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import DateTime, Enum, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSON, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin
class ExecutionRunStatus(str, enum.Enum):
QUEUED = "QUEUED"
RUNNING = "RUNNING"
SUCCEEDED = "SUCCEEDED"
FAILED = "FAILED"
PARTIAL_FAILED = "PARTIAL_FAILED"
class ExecutionStepStatus(str, enum.Enum):
PENDING = "PENDING"
RUNNING = "RUNNING"
SUCCEEDED = "SUCCEEDED"
FAILED = "FAILED"
SKIPPED = "SKIPPED"
class ExecutionRun(TimestampMixin, Base):
__tablename__ = "execution_runs"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
pipeline_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("pipelines.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
initiated_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
status: Mapped[ExecutionRunStatus] = mapped_column(
Enum(ExecutionRunStatus, name="execution_run_status"),
nullable=False,
default=ExecutionRunStatus.QUEUED,
server_default=ExecutionRunStatus.QUEUED.value,
index=True,
)
inputs: Mapped[dict[str, Any]] = mapped_column(
JSON,
nullable=False,
default=dict,
server_default="{}",
)
summary: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
error: Mapped[str | None] = mapped_column(
Text,
nullable=True,
)
started_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
)
finished_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
)
pipeline = relationship("Pipeline", lazy="select")
step_runs = relationship(
"ExecutionStepRun",
back_populates="run",
cascade="all, delete-orphan",
lazy="selectin",
)
class ExecutionStepRun(TimestampMixin, Base):
__tablename__ = "execution_step_runs"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
run_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("execution_runs.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
step: Mapped[int] = mapped_column(
Integer,
nullable=False,
index=True,
)
name: Mapped[str | None] = mapped_column(
String(512),
nullable=True,
)
capability_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
nullable=True,
index=True,
)
action_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
nullable=True,
index=True,
)
status: Mapped[ExecutionStepStatus] = mapped_column(
Enum(ExecutionStepStatus, name="execution_step_status"),
nullable=False,
default=ExecutionStepStatus.PENDING,
server_default=ExecutionStepStatus.PENDING.value,
index=True,
)
resolved_inputs: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
request_snapshot: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
response_snapshot: Mapped[dict[str, Any] | None] = mapped_column(
JSON,
nullable=True,
)
error: Mapped[str | None] = mapped_column(
Text,
nullable=True,
)
started_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
)
finished_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
nullable=True,
)
duration_ms: Mapped[int | None] = mapped_column(
Integer,
nullable=True,
)
run = relationship("ExecutionRun", back_populates="step_runs", lazy="select")
+85
View File
@@ -0,0 +1,85 @@
import enum
import uuid
from typing import Any
from sqlalchemy import Enum, ForeignKey, String, Text
from sqlalchemy.dialects.postgresql import JSON, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin
class PipelineStatus(str, enum.Enum):
DRAFT = "DRAFT"
READY = "READY"
ARCHIVED = "ARCHIVED"
class Pipeline(TimestampMixin, Base):
"""
Сценарный слой.
Коллекция нод и связей между ними — полная структура графа,
сгенерированного SynthesisService и отображаемого на канвасе (React Flow).
"""
__tablename__ = "pipelines"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
name: Mapped[str] = mapped_column(
String(512),
nullable=False,
comment="Человекочитаемое название пайплайна",
)
description: Mapped[str | None] = mapped_column(
Text,
nullable=True,
comment="Подробное описание того, что делает этот сценарий",
)
user_prompt: Mapped[str | None] = mapped_column(
Text,
nullable=True,
comment="Оригинальный текстовый запрос PM из чата, породивший этот граф",
)
nodes: Mapped[list[dict[str, Any]]] = mapped_column(
JSON,
nullable=False,
default=list,
comment="Список нод графа. Каждая нода ссылается на Capability и хранит индивидуальные параметры",
)
edges: Mapped[list[dict[str, Any]]] = mapped_column(
JSON,
nullable=False,
default=list,
comment="Список рёбер графа. Определяет порядок выполнения нод (DAG)",
)
status: Mapped[PipelineStatus] = mapped_column(
Enum(PipelineStatus, name="pipeline_status"),
nullable=False,
default=PipelineStatus.DRAFT,
server_default=PipelineStatus.DRAFT.value,
comment="Статус пайплайна: DRAFT → READY → ARCHIVED",
)
created_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
index=True,
comment="UUID пользователя (PM), создавшего или запустившего генерацию",
)
creator = relationship("User", lazy="select")
dialogs = relationship(
"PipelineDialog",
back_populates="last_pipeline",
passive_deletes=True,
lazy="selectin",
)
+119
View File
@@ -0,0 +1,119 @@
from __future__ import annotations
import enum
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import DateTime, Enum, ForeignKey, Index, String, Text, func
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin
class DialogMessageRole(str, enum.Enum):
USER = "user"
ASSISTANT = "assistant"
class PipelineDialog(TimestampMixin, Base):
__tablename__ = "pipeline_dialogs"
__table_args__ = (
Index("ix_pipeline_dialogs_user_updated_at", "user_id", "updated_at"),
)
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
)
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
title: Mapped[str | None] = mapped_column(
String(256),
nullable=True,
)
last_status: Mapped[str | None] = mapped_column(
String(32),
nullable=True,
)
last_pipeline_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("pipelines.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
last_message_preview: Mapped[str | None] = mapped_column(
Text,
nullable=True,
)
user = relationship(
"User",
back_populates="pipeline_dialogs",
lazy="select",
)
last_pipeline = relationship(
"Pipeline",
back_populates="dialogs",
lazy="select",
)
messages = relationship(
"PipelineDialogMessage",
back_populates="dialog",
cascade="all, delete-orphan",
passive_deletes=True,
lazy="selectin",
)
class PipelineDialogMessage(Base):
__tablename__ = "pipeline_dialog_messages"
__table_args__ = (
Index(
"ix_pipeline_dialog_messages_dialog_created_at",
"dialog_id",
"created_at",
),
)
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
dialog_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("pipeline_dialogs.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
role: Mapped[DialogMessageRole] = mapped_column(
Enum(DialogMessageRole, name="dialog_message_role"),
nullable=False,
index=True,
)
content: Mapped[str] = mapped_column(
Text,
nullable=False,
)
assistant_payload: Mapped[dict[str, Any] | None] = mapped_column(
JSONB,
nullable=True,
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
index=True,
)
dialog = relationship(
"PipelineDialog",
back_populates="messages",
lazy="select",
)
+39
View File
@@ -0,0 +1,39 @@
import enum
import uuid
from sqlalchemy import Boolean, Enum, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin
class UserRole(str, enum.Enum):
USER = "USER"
ADMIN = "ADMIN"
class User(TimestampMixin, Base):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email: Mapped[str] = mapped_column(String(320), unique=True, index=True, nullable=False)
full_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[UserRole] = mapped_column(
Enum(UserRole, name="user_role"),
nullable=False,
default=UserRole.USER,
server_default=UserRole.USER.value,
)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="true")
pipeline_dialogs = relationship(
"PipelineDialog",
back_populates="user",
cascade="all, delete-orphan",
passive_deletes=True,
lazy="selectin",
)
actions = relationship("Action", passive_deletes=True, lazy="selectin")
capabilities = relationship("Capability", passive_deletes=True, lazy="selectin")