Initial implementation

This commit is contained in:
Renn F
2025-12-10 02:49:54 +01:00
parent 209aa346ab
commit 0c5dac4d16
118 changed files with 38912 additions and 2 deletions
+103
View File
@@ -0,0 +1,103 @@
"""
Alembic Environment Configuration
Handles both online and offline migrations with async support.
"""
import asyncio
from logging.config import fileConfig
from alembic import context
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from roboco.config import settings
from roboco.db.base import Base
# Import all models to ensure they're registered with Base.metadata
from roboco.db import tables # noqa: F401
# Alembic Config object
config = context.config
# Override sqlalchemy.url with settings
config.set_main_option("sqlalchemy.url", settings.database_url_sync)
# Interpret the config file for Python logging
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Target metadata for autogenerate
target_metadata = Base.metadata
def run_migrations_offline() -> None:
"""
Run migrations in 'offline' mode.
This configures the context with just a URL and not an Engine,
though an Engine is acceptable here as well. By skipping the Engine
creation we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
compare_server_default=True,
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
"""Run migrations with the given connection."""
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True,
)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
"""
Run migrations in 'online' mode with async engine.
In this scenario we need to create an Engine and associate a
connection with the context.
"""
configuration = config.get_section(config.config_ini_section, {})
configuration["sqlalchemy.url"] = settings.database_url
connectable = async_engine_from_config(
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+26
View File
@@ -0,0 +1,26 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
View File
+573
View File
@@ -0,0 +1,573 @@
"""Initial schema - all tables
Revision ID: 001_initial_schema
Revises:
Create Date: 2025-12-09
Creates all tables for the RoboCo AI Agents Company system:
- agents: AI agent definitions and state
- tasks: Work items with lifecycle management
- channels: Communication channels
- groups: Role-based groups within channels
- sessions: Bounded message sessions
- messages: Extracted messages
- notifications: Formal notifications
- journals: Agent personal logs
- journal_entries: Individual log entries
- handoffs: Documentation handoffs
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "001_initial_schema"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# ==========================================================================
# AGENTS TABLE
# ==========================================================================
op.create_table(
"agents",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("slug", sa.String(50), nullable=False, unique=True, index=True),
sa.Column(
"role",
sa.Enum(
"ceo",
"product_owner",
"head_marketing",
"auditor",
"main_pm",
"cell_pm",
"developer",
"qa",
"documenter",
name="agentrole",
),
nullable=False,
),
sa.Column(
"team",
sa.Enum("backend", "frontend", "ux_ui", "board", name="team"),
nullable=True,
),
sa.Column(
"status",
sa.Enum("active", "idle", "offline", name="agentstatus"),
nullable=False,
server_default="offline",
),
sa.Column("current_task_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("model_config", postgresql.JSON(), nullable=False),
sa.Column("system_prompt", sa.Text(), nullable=False),
sa.Column("capabilities", postgresql.ARRAY(sa.String()), server_default="{}"),
sa.Column("permissions", postgresql.JSON(), server_default="{}"),
sa.Column("metrics", postgresql.JSON(), server_default="{}"),
sa.Column("journal_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(), nullable=True),
)
# ==========================================================================
# TASKS TABLE
# ==========================================================================
op.create_table(
"tasks",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("title", sa.String(200), nullable=False),
sa.Column("description", sa.Text(), nullable=False),
sa.Column("acceptance_criteria", postgresql.ARRAY(sa.String()), nullable=False),
sa.Column(
"status",
sa.Enum(
"pending",
"claimed",
"in_progress",
"blocked",
"paused",
"verifying",
"needs_revision",
"awaiting_qa",
"awaiting_documentation",
"completed",
"cancelled",
name="taskstatus",
),
nullable=False,
server_default="pending",
index=True,
),
sa.Column("priority", sa.Integer(), nullable=False, server_default="2"),
sa.Column(
"created_by",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("agents.id"),
nullable=False,
),
sa.Column(
"assigned_to",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("agents.id", ondelete="SET NULL"),
nullable=True,
index=True,
),
sa.Column(
"team",
sa.Enum("backend", "frontend", "ux_ui", "board", name="team", create_type=False),
nullable=False,
index=True,
),
sa.Column(
"parent_task_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("tasks.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column("dependency_ids", postgresql.ARRAY(postgresql.UUID(as_uuid=True)), server_default="{}"),
sa.Column("blocker_ids", postgresql.ARRAY(postgresql.UUID(as_uuid=True)), server_default="{}"),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.Column("claimed_at", sa.DateTime(), nullable=True),
sa.Column("started_at", sa.DateTime(), nullable=True),
sa.Column("completed_at", sa.DateTime(), nullable=True),
sa.Column("target_date", sa.DateTime(), nullable=True),
sa.Column("plan", postgresql.JSON(), nullable=True),
sa.Column(
"estimated_complexity",
sa.Enum("low", "medium", "high", name="complexity"),
nullable=False,
server_default="medium",
),
sa.Column("execution_log", postgresql.JSON(), server_default="{}"),
sa.Column("checkpoints", postgresql.JSON(), server_default="[]"),
sa.Column("progress_updates", postgresql.JSON(), server_default="[]"),
sa.Column("commits", postgresql.JSON(), server_default="[]"),
sa.Column("documents", postgresql.JSON(), server_default="[]"),
sa.Column("outputs", postgresql.JSON(), server_default="[]"),
sa.Column("dev_notes", sa.Text(), nullable=True),
sa.Column("qa_notes", sa.Text(), nullable=True),
sa.Column("auditor_notes", sa.Text(), nullable=True),
sa.Column("self_verified", sa.Boolean(), server_default="false"),
sa.Column("qa_verified", sa.Boolean(), nullable=True),
sa.Column("quick_context", sa.Text(), nullable=True),
)
# Add foreign key for agents.current_task_id after tasks table exists
op.create_foreign_key(
"fk_agents_current_task",
"agents",
"tasks",
["current_task_id"],
["id"],
ondelete="SET NULL",
)
# ==========================================================================
# CHANNELS TABLE
# ==========================================================================
op.create_table(
"channels",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column("slug", sa.String(50), nullable=False, unique=True, index=True),
sa.Column(
"type",
sa.Enum("cell", "cross_cell", "management", "special", name="channeltype"),
nullable=False,
),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("topic", sa.String(500), nullable=True),
sa.Column("members", postgresql.ARRAY(postgresql.UUID(as_uuid=True)), server_default="{}"),
sa.Column("writers", postgresql.ARRAY(postgresql.UUID(as_uuid=True)), server_default="{}"),
sa.Column("silent_observers", postgresql.ARRAY(postgresql.UUID(as_uuid=True)), server_default="{}"),
sa.Column("is_archived", sa.Boolean(), server_default="false"),
sa.Column("is_private", sa.Boolean(), server_default="false"),
sa.Column("allow_threads", sa.Boolean(), server_default="true"),
sa.Column("allow_reactions", sa.Boolean(), server_default="true"),
sa.Column("message_retention_days", sa.Integer(), nullable=True, server_default="90"),
sa.Column("max_message_length", sa.Integer(), server_default="10000"),
sa.Column("message_count", sa.Integer(), server_default="0"),
sa.Column("group_count", sa.Integer(), server_default="0"),
sa.Column("last_activity", sa.DateTime(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(), nullable=True),
)
# ==========================================================================
# GROUPS TABLE
# ==========================================================================
op.create_table(
"groups",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("name", sa.String(100), nullable=False),
sa.Column(
"channel_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("channels.id", ondelete="CASCADE"),
nullable=False,
index=True,
),
sa.Column("allowed_roles", postgresql.ARRAY(sa.String()), server_default="{}"),
sa.Column("hierarchy_level", sa.Integer(), server_default="4"),
sa.Column("members", postgresql.ARRAY(postgresql.UUID(as_uuid=True)), server_default="{}"),
sa.Column("is_active", sa.Boolean(), server_default="true"),
sa.Column("active_session_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("default_session_config", postgresql.JSON(), server_default="{}"),
sa.Column("total_sessions", sa.Integer(), server_default="0"),
sa.Column("total_messages", sa.Integer(), server_default="0"),
sa.Column("last_activity", sa.DateTime(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(), nullable=True),
)
# ==========================================================================
# SESSIONS TABLE
# ==========================================================================
op.create_table(
"sessions",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"group_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("groups.id", ondelete="CASCADE"),
nullable=False,
index=True,
),
sa.Column("max_time_window", sa.Interval(), nullable=True),
sa.Column("max_message_count", sa.Integer(), nullable=True, server_default="100"),
sa.Column("max_content_length", sa.Integer(), nullable=True, server_default="50000"),
sa.Column("timeout_seconds", sa.Integer(), server_default="300"),
sa.Column(
"status",
sa.Enum("active", "closed", "timed_out", name="sessionstatus"),
nullable=False,
server_default="active",
index=True,
),
sa.Column("started_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("last_activity_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("closed_at", sa.DateTime(), nullable=True),
sa.Column("message_count", sa.Integer(), server_default="0"),
sa.Column("total_content_length", sa.Integer(), server_default="0"),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
)
# ==========================================================================
# MESSAGES TABLE
# ==========================================================================
op.create_table(
"messages",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"agent_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("agents.id"),
nullable=False,
index=True,
),
sa.Column(
"channel_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("channels.id", ondelete="CASCADE"),
nullable=False,
index=True,
),
sa.Column(
"group_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("groups.id", ondelete="CASCADE"),
nullable=False,
index=True,
),
sa.Column(
"session_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
index=True,
),
sa.Column(
"type",
sa.Enum(
"reasoning",
"dialogue",
"decision",
"action",
"blocker",
"technical",
name="messagetype",
),
nullable=False,
index=True,
),
sa.Column("content", sa.Text(), nullable=False),
sa.Column("content_length", sa.Integer(), nullable=False),
sa.Column("is_reply", sa.Boolean(), server_default="false"),
sa.Column(
"reply_to",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("messages.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column("mentions", postgresql.ARRAY(postgresql.UUID(as_uuid=True)), server_default="{}"),
sa.Column(
"task_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("tasks.id", ondelete="SET NULL"),
nullable=True,
index=True,
),
sa.Column("commit_ref", sa.String(40), nullable=True),
sa.Column("timestamp", sa.DateTime(), nullable=False, server_default=sa.func.now(), index=True),
sa.Column("confidence", sa.Float(), server_default="1.0"),
sa.Column("raw_excerpt", sa.Text(), nullable=True),
sa.Column("edited_at", sa.DateTime(), nullable=True),
sa.Column("edit_history", postgresql.JSON(), server_default="[]"),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
)
# ==========================================================================
# NOTIFICATIONS TABLE
# ==========================================================================
op.create_table(
"notifications",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"type",
sa.Enum(
"task_assignment",
"priority_change",
"blocker_escalation",
"review_request",
"documentation_request",
"alert",
"broadcast",
name="notificationtype",
),
nullable=False,
index=True,
),
sa.Column(
"priority",
sa.Enum("normal", "high", "urgent", name="notificationpriority"),
nullable=False,
server_default="normal",
),
sa.Column(
"from_agent",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("agents.id"),
nullable=False,
),
sa.Column("to_agents", postgresql.ARRAY(postgresql.UUID(as_uuid=True)), nullable=False),
sa.Column("subject", sa.String(200), nullable=False),
sa.Column("body", sa.Text(), nullable=False),
sa.Column("requires_ack", sa.Boolean(), server_default="true"),
sa.Column("acked_by", postgresql.ARRAY(postgresql.UUID(as_uuid=True)), server_default="{}"),
sa.Column("acked_at", postgresql.JSON(), server_default="{}"),
sa.Column(
"related_task_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("tasks.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column("related_message_ids", postgresql.ARRAY(postgresql.UUID(as_uuid=True)), server_default="{}"),
sa.Column("timestamp", sa.DateTime(), nullable=False, server_default=sa.func.now(), index=True),
sa.Column("expires_at", sa.DateTime(), nullable=True),
sa.Column("read_by", postgresql.ARRAY(postgresql.UUID(as_uuid=True)), server_default="{}"),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
)
# ==========================================================================
# JOURNALS TABLE
# ==========================================================================
op.create_table(
"journals",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"agent_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("agents.id", ondelete="CASCADE"),
nullable=False,
unique=True,
),
sa.Column("total_entries", sa.Integer(), server_default="0"),
sa.Column("last_entry_at", sa.DateTime(), nullable=True),
sa.Column("latest_summary", sa.Text(), nullable=True),
sa.Column("summary_updated_at", sa.DateTime(), nullable=True),
sa.Column("entries_by_type", postgresql.JSON(), server_default="{}"),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(), nullable=True),
)
# ==========================================================================
# JOURNAL ENTRIES TABLE
# ==========================================================================
op.create_table(
"journal_entries",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"journal_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("journals.id", ondelete="CASCADE"),
nullable=False,
index=True,
),
sa.Column(
"type",
sa.Enum(
"task_reflection",
"decision_log",
"learning",
"struggle",
"general",
name="journalentrytype",
),
nullable=False,
index=True,
),
sa.Column("title", sa.String(200), nullable=False),
sa.Column("content", sa.Text(), nullable=False),
sa.Column(
"task_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("tasks.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column(
"session_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("sessions.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column("timestamp", sa.DateTime(), nullable=False, server_default=sa.func.now(), index=True),
sa.Column("tags", postgresql.ARRAY(sa.String()), server_default="{}"),
sa.Column("sentiment", sa.String(50), nullable=True),
sa.Column("is_private", sa.Boolean(), server_default="false"),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(), nullable=True),
)
# ==========================================================================
# HANDOFFS TABLE
# ==========================================================================
op.create_table(
"handoffs",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"task_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("tasks.id", ondelete="CASCADE"),
nullable=False,
index=True,
),
sa.Column("summary", sa.Text(), nullable=False),
sa.Column("new_functionality", postgresql.ARRAY(sa.String()), server_default="{}"),
sa.Column("modified_behavior", postgresql.ARRAY(sa.String()), server_default="{}"),
sa.Column("breaking_changes", postgresql.ARRAY(sa.String()), server_default="{}"),
sa.Column("required_docs", postgresql.JSON(), server_default="[]"),
sa.Column("optional_docs", postgresql.JSON(), server_default="[]"),
sa.Column("commits", postgresql.JSON(), server_default="[]"),
sa.Column("new_files", postgresql.JSON(), server_default="[]"),
sa.Column("modified_files", postgresql.JSON(), server_default="[]"),
sa.Column("key_conversations", postgresql.JSON(), server_default="[]"),
sa.Column("code_samples", postgresql.JSON(), server_default="[]"),
sa.Column("gotchas", postgresql.JSON(), server_default="[]"),
sa.Column("related_docs", postgresql.ARRAY(sa.String()), server_default="{}"),
sa.Column("changelog_entry", sa.Text(), nullable=True),
sa.Column("key_learnings", postgresql.ARRAY(sa.String()), server_default="{}"),
sa.Column("key_decisions", postgresql.JSON(), server_default="[]"),
sa.Column("questions", postgresql.ARRAY(sa.String()), server_default="{}"),
sa.Column("dev_notes_location", sa.String(500), nullable=True),
sa.Column(
"status",
sa.Enum("pending", "claimed", "in_progress", "completed", name="handoffstatus"),
nullable=False,
server_default="pending",
index=True,
),
sa.Column(
"assigned_to",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("agents.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.Column("claimed_at", sa.DateTime(), nullable=True),
sa.Column("completed_at", sa.DateTime(), nullable=True),
sa.Column("documenter_notes", sa.Text(), nullable=True),
sa.UniqueConstraint("task_id", name="uq_handoffs_task_id"),
)
# ==========================================================================
# PERFORMANCE INDEXES
# ==========================================================================
# Composite indexes for common queries
op.create_index(
"ix_tasks_team_status",
"tasks",
["team", "status"],
)
op.create_index(
"ix_tasks_assigned_status",
"tasks",
["assigned_to", "status"],
)
op.create_index(
"ix_messages_channel_timestamp",
"messages",
["channel_id", sa.text("timestamp DESC")],
)
op.create_index(
"ix_notifications_to_timestamp",
"notifications",
[sa.text("timestamp DESC")],
)
def downgrade() -> None:
# Drop indexes
op.drop_index("ix_notifications_to_timestamp", table_name="notifications")
op.drop_index("ix_messages_channel_timestamp", table_name="messages")
op.drop_index("ix_tasks_assigned_status", table_name="tasks")
op.drop_index("ix_tasks_team_status", table_name="tasks")
# Drop tables in reverse order
op.drop_table("handoffs")
op.drop_table("journal_entries")
op.drop_table("journals")
op.drop_table("notifications")
op.drop_table("messages")
op.drop_table("sessions")
op.drop_table("groups")
op.drop_table("channels")
# Drop FK before dropping tasks
op.drop_constraint("fk_agents_current_task", "agents", type_="foreignkey")
op.drop_table("tasks")
op.drop_table("agents")
# Drop enums
op.execute("DROP TYPE IF EXISTS handoffstatus")
op.execute("DROP TYPE IF EXISTS journalentrytype")
op.execute("DROP TYPE IF EXISTS notificationpriority")
op.execute("DROP TYPE IF EXISTS notificationtype")
op.execute("DROP TYPE IF EXISTS messagetype")
op.execute("DROP TYPE IF EXISTS sessionstatus")
op.execute("DROP TYPE IF EXISTS channeltype")
op.execute("DROP TYPE IF EXISTS complexity")
op.execute("DROP TYPE IF EXISTS taskstatus")
op.execute("DROP TYPE IF EXISTS agentstatus")
op.execute("DROP TYPE IF EXISTS team")
op.execute("DROP TYPE IF EXISTS agentrole")