Initial commit: VoIdeaAI - voice-first AI idea assistant

This commit is contained in:
2026-05-13 12:51:42 +03:00
commit 688d043dad
421 changed files with 47915 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
"""Initial migration — create all tables
Revision ID: 001
Revises:
Create Date: 2026-05-11
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = "001"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"users",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("email", sa.String(255), unique=True, nullable=False, index=True),
sa.Column("password_hash", sa.String(255), nullable=True),
sa.Column("display_name", sa.String(255), nullable=False),
sa.Column("avatar_url", sa.String(512), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("is_superuser", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("oauth_provider", sa.String(50), nullable=True),
sa.Column("oauth_id", sa.String(255), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"agent_configs",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("agent_name", sa.String(100), unique=True, nullable=False, index=True),
sa.Column("is_enabled", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("version", sa.String(20), nullable=False, server_default=sa.text("'1.0.0'")),
sa.Column("checksum", sa.String(64), nullable=True),
sa.Column("config", sa.Text(), nullable=True),
sa.Column("last_run_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"backlog_tasks",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("title", sa.String(255), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("priority", sa.String(20), nullable=False, server_default=sa.text("'medium'"), index=True),
sa.Column("status", sa.String(20), nullable=False, server_default=sa.text("'pending'"), index=True),
sa.Column("source_agent", sa.String(100), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"ideas",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False, index=True),
sa.Column("title", sa.String(255), nullable=False, index=True),
sa.Column("content", sa.Text(), nullable=False),
sa.Column("status", sa.String(20), nullable=False, server_default=sa.text("'draft'"), index=True),
sa.Column("tags", postgresql.ARRAY(sa.String(50)), nullable=True),
sa.Column("is_public", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"log_entries",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("level", sa.String(20), nullable=False, index=True),
sa.Column("source", sa.String(100), nullable=False, index=True),
sa.Column("message", sa.Text(), nullable=False),
sa.Column("details", sa.Text(), nullable=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=True, index=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_foreign_key(
"fk_ideas_user_id", "ideas", "users",
["user_id"], ["id"], ondelete="CASCADE",
)
op.create_foreign_key(
"fk_log_entries_user_id", "log_entries", "users",
["user_id"], ["id"], ondelete="SET NULL",
)
op.create_table(
"conductor_interactions",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=True, index=True),
sa.Column("input_text", sa.Text(), nullable=False),
sa.Column("detected_intent", sa.String(100), nullable=False),
sa.Column("selected_agent", sa.String(100), nullable=False),
sa.Column("response_text", sa.Text(), nullable=False),
sa.Column("user_rating", sa.Integer(), nullable=True),
sa.Column("confidence_score", sa.Integer(), nullable=False, server_default=sa.text("80")),
sa.Column("verification_status", sa.String(20), nullable=False, server_default=sa.text("'verified'")),
sa.Column("was_auto_routed", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("processing_time_ms", sa.Float(), nullable=False, server_default=sa.text("0.0")),
sa.Column("context", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_foreign_key(
"fk_conductor_user_id", "conductor_interactions", "users",
["user_id"], ["id"], ondelete="SET NULL",
)
# ── sessions (чат-сессии = идеи) ──
op.create_table(
"sessions",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False, index=True),
sa.Column("title", sa.String(255), nullable=False, server_default=sa.text("'Новое обсуждение'")),
sa.Column("status", sa.String(20), nullable=False, server_default=sa.text("'active'"), index=True),
sa.Column("idea_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_foreign_key(
"fk_sessions_user_id", "sessions", "users",
["user_id"], ["id"], ondelete="CASCADE",
)
op.create_foreign_key(
"fk_sessions_idea_id", "sessions", "ideas",
["idea_id"], ["id"], ondelete="SET NULL",
)
# ── session_id в conductor_interactions ──
op.add_column(
"conductor_interactions",
sa.Column("session_id", postgresql.UUID(as_uuid=True), nullable=True, index=True),
)
op.create_foreign_key(
"fk_conductor_session_id", "conductor_interactions", "sessions",
["session_id"], ["id"], ondelete="SET NULL",
)
# ── voice_commands (пользовательские голосовые команды) ──
op.create_table(
"voice_commands",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False, index=True),
sa.Column("phrase", sa.String(255), nullable=False),
sa.Column("action", sa.String(50), nullable=False),
sa.Column("agent_name", sa.String(100), nullable=True),
sa.Column("count", sa.Integer(), nullable=False, server_default=sa.text("0")),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_foreign_key(
"fk_voice_commands_user_id", "voice_commands", "users",
["user_id"], ["id"], ondelete="CASCADE",
)
def downgrade() -> None:
op.drop_table("voice_commands")
op.drop_table("sessions")
op.drop_constraint("fk_conductor_session_id", "conductor_interactions", type_="foreignkey")
op.drop_column("conductor_interactions", "session_id")
op.drop_table("conductor_interactions")
op.drop_table("log_entries")
op.drop_table("ideas")
op.drop_table("backlog_tasks")
op.drop_table("agent_configs")
op.drop_table("users")
@@ -0,0 +1,117 @@
"""v2.0: Add role system, feedback, tariffs, backlog category
Revision ID: 002
Revises: 001
Create Date: 2026-05-11
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = "002"
down_revision: Union[str, None] = "001"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ── users: add v2.0 role system ──
op.add_column("users", sa.Column("role", sa.String(20),
server_default=sa.text("'user'"), nullable=False, index=True))
op.add_column("users", sa.Column("is_owner", sa.Boolean(),
server_default=sa.text("false"), nullable=False))
op.add_column("users", sa.Column("permissions", postgresql.JSONB,
nullable=True))
op.add_column("users", sa.Column("accepted_terms_at",
sa.DateTime(timezone=True), nullable=True))
op.add_column("users", sa.Column("accepted_terms_version",
sa.String(20), nullable=True))
# ── agent_configs: add description ──
op.add_column("agent_configs", sa.Column("description", sa.Text(),
nullable=True, server_default=sa.text("''")))
# ── backlog_tasks: add category ──
op.add_column("backlog_tasks", sa.Column("category", sa.String(50),
server_default=sa.text("'general'"), nullable=False, index=True))
# ── feedback ──
op.create_table(
"feedback",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=True, index=True),
sa.Column("text", sa.Text(), nullable=False),
sa.Column("page_url", sa.String(512), nullable=True),
sa.Column("status", sa.String(20), nullable=False,
server_default=sa.text("'new'"), index=True),
sa.Column("created_at", sa.DateTime(timezone=True),
server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True),
server_default=sa.func.now()),
)
op.create_foreign_key(
"fk_feedback_user_id", "feedback", "users",
["user_id"], ["id"], ondelete="SET NULL",
)
# ── tariff_plans ──
op.create_table(
"tariff_plans",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("code", sa.String(50), unique=True, nullable=False, index=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("price_monthly", sa.Numeric(10, 2), nullable=False,
server_default=sa.text("0")),
sa.Column("price_yearly", sa.Numeric(10, 2), nullable=True),
sa.Column("features", postgresql.JSONB, nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False,
server_default=sa.text("true")),
sa.Column("sort_order", sa.Integer(), nullable=False,
server_default=sa.text("0")),
sa.Column("created_at", sa.DateTime(timezone=True),
server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True),
server_default=sa.func.now()),
)
# ── user_subscriptions ──
op.create_table(
"user_subscriptions",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True), unique=True,
nullable=False, index=True),
sa.Column("plan_id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("status", sa.String(20), nullable=False,
server_default=sa.text("'active'"), index=True),
sa.Column("current_period_start", sa.DateTime(timezone=True), nullable=True),
sa.Column("current_period_end", sa.DateTime(timezone=True), nullable=True),
sa.Column("canceled_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True),
server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True),
server_default=sa.func.now()),
)
op.create_foreign_key(
"fk_user_subscriptions_user_id", "user_subscriptions", "users",
["user_id"], ["id"], ondelete="CASCADE",
)
op.create_foreign_key(
"fk_user_subscriptions_plan_id", "user_subscriptions", "tariff_plans",
["plan_id"], ["id"], ondelete="RESTRICT",
)
def downgrade() -> None:
op.drop_table("user_subscriptions")
op.drop_table("tariff_plans")
op.drop_table("feedback")
op.drop_column("backlog_tasks", "category")
op.drop_column("agent_configs", "description")
op.drop_column("users", "accepted_terms_version")
op.drop_column("users", "accepted_terms_at")
op.drop_column("users", "permissions")
op.drop_column("users", "is_owner")
op.drop_column("users", "role")
@@ -0,0 +1,51 @@
"""Add pipeline_stats table and User.pipeline_tuning
Revision ID: 003
Revises: 002
Create Date: 2026-05-11
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = "003"
down_revision: Union[str, None] = "002"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ── users: add pipeline_tuning ──
op.add_column("users", sa.Column(
"pipeline_tuning", postgresql.JSONB(), nullable=True
))
# ── pipeline_stats ──
op.create_table(
"pipeline_stats",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("user_id", sa.String(36),
sa.ForeignKey("users.id", ondelete="CASCADE"),
nullable=True, index=True),
sa.Column("stage", sa.String(50), nullable=False, index=True),
sa.Column("passed", sa.Boolean(), nullable=False),
sa.Column("reason", sa.String(255), nullable=True),
sa.Column("duration_ms", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True),
nullable=False),
)
# composite index for stats aggregation
op.create_index(
"ix_pipeline_stats_user_stage",
"pipeline_stats",
["user_id", "stage"],
)
def downgrade() -> None:
op.drop_index("ix_pipeline_stats_user_stage", table_name="pipeline_stats")
op.drop_table("pipeline_stats")
op.drop_column("users", "pipeline_tuning")
+34
View File
@@ -0,0 +1,34 @@
"""Add bot_commands table
Revision ID: 004
Revises: 003
Create Date: 2026-05-12
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = "004"
down_revision: Union[str, None] = "003"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"bot_commands",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("name", sa.String(50), unique=True, nullable=False, index=True),
sa.Column("description", sa.String(255), nullable=False),
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("requires_auth", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
def downgrade() -> None:
op.drop_table("bot_commands")
@@ -0,0 +1,27 @@
"""Add public_slug column to ideas table
Revision ID: 005
Revises: 004
Create Date: 2026-05-12
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "005"
down_revision: Union[str, None] = "004"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
"ideas",
sa.Column("public_slug", sa.String(64), unique=True, nullable=True, index=True),
)
def downgrade() -> None:
op.drop_column("ideas", "public_slug")