From 89d24282520256b19bbd77cf2d54b7203c80c975 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Thu, 5 Feb 2026 14:39:34 +0530 Subject: [PATCH] feat: add board goals, memory, approvals, onboarding models --- .../3b9b2f1a6c2d_board_lead_orchestration.py | 145 ++++++++++++++++++ backend/app/models/__init__.py | 8 + backend/app/models/agents.py | 1 + backend/app/models/approvals.py | 22 +++ backend/app/models/board_memory.py | 18 +++ backend/app/models/board_onboarding.py | 20 +++ backend/app/models/boards.py | 7 + backend/app/models/task_fingerprints.py | 16 ++ backend/app/models/tasks.py | 2 + 9 files changed, 239 insertions(+) create mode 100644 backend/alembic/versions/3b9b2f1a6c2d_board_lead_orchestration.py create mode 100644 backend/app/models/approvals.py create mode 100644 backend/app/models/board_memory.py create mode 100644 backend/app/models/board_onboarding.py create mode 100644 backend/app/models/task_fingerprints.py diff --git a/backend/alembic/versions/3b9b2f1a6c2d_board_lead_orchestration.py b/backend/alembic/versions/3b9b2f1a6c2d_board_lead_orchestration.py new file mode 100644 index 00000000..8f9dbced --- /dev/null +++ b/backend/alembic/versions/3b9b2f1a6c2d_board_lead_orchestration.py @@ -0,0 +1,145 @@ +"""board lead orchestration + +Revision ID: 3b9b2f1a6c2d +Revises: 9f2c1a7b0d3e +Create Date: 2026-02-05 14:45:00.000000 +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "3b9b2f1a6c2d" +down_revision = "9f2c1a7b0d3e" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("boards", sa.Column("board_type", sa.String(), server_default="goal", nullable=False)) + op.add_column("boards", sa.Column("objective", sa.Text(), nullable=True)) + op.add_column("boards", sa.Column("success_metrics", sa.JSON(), nullable=True)) + op.add_column("boards", sa.Column("target_date", sa.DateTime(), nullable=True)) + op.add_column( + "boards", + sa.Column("goal_confirmed", sa.Boolean(), server_default=sa.text("false"), nullable=False), + ) + op.add_column("boards", sa.Column("goal_source", sa.Text(), nullable=True)) + + op.add_column( + "agents", + sa.Column("is_board_lead", sa.Boolean(), server_default=sa.text("false"), nullable=False), + ) + + op.add_column( + "tasks", + sa.Column("auto_created", sa.Boolean(), server_default=sa.text("false"), nullable=False), + ) + op.add_column("tasks", sa.Column("auto_reason", sa.Text(), nullable=True)) + + op.create_table( + "board_memory", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("board_id", sa.Uuid(), nullable=False), + sa.Column("content", sa.Text(), nullable=False), + sa.Column("tags", sa.JSON(), nullable=True), + sa.Column("source", sa.Text(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["board_id"], ["boards.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_board_memory_board_id", "board_memory", ["board_id"], unique=False) + + op.create_table( + "approvals", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("board_id", sa.Uuid(), nullable=False), + sa.Column("agent_id", sa.Uuid(), nullable=True), + sa.Column("action_type", sa.String(), nullable=False), + sa.Column("payload", sa.JSON(), nullable=True), + sa.Column("confidence", sa.Integer(), nullable=False), + sa.Column("rubric_scores", sa.JSON(), nullable=True), + sa.Column("status", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("resolved_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(["agent_id"], ["agents.id"]), + sa.ForeignKeyConstraint(["board_id"], ["boards.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_approvals_board_id", "approvals", ["board_id"], unique=False) + op.create_index("ix_approvals_agent_id", "approvals", ["agent_id"], unique=False) + op.create_index("ix_approvals_status", "approvals", ["status"], unique=False) + + op.create_table( + "board_onboarding_sessions", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("board_id", sa.Uuid(), nullable=False), + sa.Column("session_key", sa.String(), nullable=False), + sa.Column("status", sa.String(), nullable=False), + sa.Column("messages", sa.JSON(), nullable=True), + sa.Column("draft_goal", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["board_id"], ["boards.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + "ix_board_onboarding_sessions_board_id", + "board_onboarding_sessions", + ["board_id"], + unique=False, + ) + op.create_index( + "ix_board_onboarding_sessions_status", + "board_onboarding_sessions", + ["status"], + unique=False, + ) + + op.create_table( + "task_fingerprints", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("board_id", sa.Uuid(), nullable=False), + sa.Column("fingerprint_hash", sa.String(), nullable=False), + sa.Column("task_id", sa.Uuid(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["board_id"], ["boards.id"]), + sa.ForeignKeyConstraint(["task_id"], ["tasks.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + "ix_task_fingerprints_board_hash", + "task_fingerprints", + ["board_id", "fingerprint_hash"], + unique=True, + ) + + +def downgrade() -> None: + op.drop_index("ix_task_fingerprints_board_hash", table_name="task_fingerprints") + op.drop_table("task_fingerprints") + op.drop_index( + "ix_board_onboarding_sessions_status", table_name="board_onboarding_sessions" + ) + op.drop_index( + "ix_board_onboarding_sessions_board_id", table_name="board_onboarding_sessions" + ) + op.drop_table("board_onboarding_sessions") + op.drop_index("ix_approvals_status", table_name="approvals") + op.drop_index("ix_approvals_agent_id", table_name="approvals") + op.drop_index("ix_approvals_board_id", table_name="approvals") + op.drop_table("approvals") + op.drop_index("ix_board_memory_board_id", table_name="board_memory") + op.drop_table("board_memory") + op.drop_column("tasks", "auto_reason") + op.drop_column("tasks", "auto_created") + op.drop_column("agents", "is_board_lead") + op.drop_column("boards", "goal_source") + op.drop_column("boards", "goal_confirmed") + op.drop_column("boards", "target_date") + op.drop_column("boards", "success_metrics") + op.drop_column("boards", "objective") + op.drop_column("boards", "board_type") diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 9f0e5c35..3ed49dbd 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,15 +1,23 @@ from app.models.activity_events import ActivityEvent from app.models.agents import Agent +from app.models.approvals import Approval +from app.models.board_memory import BoardMemory +from app.models.board_onboarding import BoardOnboardingSession from app.models.boards import Board from app.models.gateways import Gateway from app.models.tasks import Task +from app.models.task_fingerprints import TaskFingerprint from app.models.users import User __all__ = [ "ActivityEvent", "Agent", + "Approval", + "BoardMemory", + "BoardOnboardingSession", "Board", "Gateway", "Task", + "TaskFingerprint", "User", ] diff --git a/backend/app/models/agents.py b/backend/app/models/agents.py index efededb5..d7d56427 100644 --- a/backend/app/models/agents.py +++ b/backend/app/models/agents.py @@ -27,5 +27,6 @@ class Agent(SQLModel, table=True): delete_requested_at: datetime | None = Field(default=None) delete_confirm_token_hash: str | None = Field(default=None, index=True) last_seen_at: datetime | None = Field(default=None) + is_board_lead: bool = Field(default=False, index=True) created_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/app/models/approvals.py b/backend/app/models/approvals.py new file mode 100644 index 00000000..6fdf6622 --- /dev/null +++ b/backend/app/models/approvals.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from datetime import datetime +from uuid import UUID, uuid4 + +from sqlalchemy import JSON, Column +from sqlmodel import Field, SQLModel + + +class Approval(SQLModel, table=True): + __tablename__ = "approvals" + + id: UUID = Field(default_factory=uuid4, primary_key=True) + board_id: UUID = Field(foreign_key="boards.id", index=True) + agent_id: UUID | None = Field(default=None, foreign_key="agents.id", index=True) + action_type: str + payload: dict[str, object] | None = Field(default=None, sa_column=Column(JSON)) + confidence: int + rubric_scores: dict[str, int] | None = Field(default=None, sa_column=Column(JSON)) + status: str = Field(default="pending", index=True) + created_at: datetime = Field(default_factory=datetime.utcnow) + resolved_at: datetime | None = None diff --git a/backend/app/models/board_memory.py b/backend/app/models/board_memory.py new file mode 100644 index 00000000..abf63866 --- /dev/null +++ b/backend/app/models/board_memory.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from datetime import datetime +from uuid import UUID, uuid4 + +from sqlalchemy import JSON, Column +from sqlmodel import Field, SQLModel + + +class BoardMemory(SQLModel, table=True): + __tablename__ = "board_memory" + + id: UUID = Field(default_factory=uuid4, primary_key=True) + board_id: UUID = Field(foreign_key="boards.id", index=True) + content: str + tags: list[str] | None = Field(default=None, sa_column=Column(JSON)) + source: str | None = None + created_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/app/models/board_onboarding.py b/backend/app/models/board_onboarding.py new file mode 100644 index 00000000..4c634287 --- /dev/null +++ b/backend/app/models/board_onboarding.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from datetime import datetime +from uuid import UUID, uuid4 + +from sqlalchemy import JSON, Column +from sqlmodel import Field, SQLModel + + +class BoardOnboardingSession(SQLModel, table=True): + __tablename__ = "board_onboarding_sessions" + + id: UUID = Field(default_factory=uuid4, primary_key=True) + board_id: UUID = Field(foreign_key="boards.id", index=True) + session_key: str + status: str = Field(default="active", index=True) + messages: list[dict[str, object]] | None = Field(default=None, sa_column=Column(JSON)) + draft_goal: dict[str, object] | None = Field(default=None, sa_column=Column(JSON)) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/app/models/boards.py b/backend/app/models/boards.py index 2e0ad89d..a7f8b788 100644 --- a/backend/app/models/boards.py +++ b/backend/app/models/boards.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime from uuid import UUID, uuid4 +from sqlalchemy import JSON, Column from sqlmodel import Field from app.models.tenancy import TenantScoped @@ -15,5 +16,11 @@ class Board(TenantScoped, table=True): name: str slug: str = Field(index=True) gateway_id: UUID | None = Field(default=None, foreign_key="gateways.id", index=True) + board_type: str = Field(default="goal", index=True) + objective: str | None = None + success_metrics: dict[str, object] | None = Field(default=None, sa_column=Column(JSON)) + target_date: datetime | None = None + goal_confirmed: bool = Field(default=False) + goal_source: str | None = None created_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/app/models/task_fingerprints.py b/backend/app/models/task_fingerprints.py new file mode 100644 index 00000000..3a1cc890 --- /dev/null +++ b/backend/app/models/task_fingerprints.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from datetime import datetime +from uuid import UUID, uuid4 + +from sqlmodel import Field, SQLModel + + +class TaskFingerprint(SQLModel, table=True): + __tablename__ = "task_fingerprints" + + id: UUID = Field(default_factory=uuid4, primary_key=True) + board_id: UUID = Field(foreign_key="boards.id", index=True) + fingerprint_hash: str = Field(index=True) + task_id: UUID = Field(foreign_key="tasks.id") + created_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/backend/app/models/tasks.py b/backend/app/models/tasks.py index 1c35df24..a599e303 100644 --- a/backend/app/models/tasks.py +++ b/backend/app/models/tasks.py @@ -23,6 +23,8 @@ class Task(TenantScoped, table=True): created_by_user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True) assigned_agent_id: UUID | None = Field(default=None, foreign_key="agents.id", index=True) + auto_created: bool = Field(default=False) + auto_reason: str | None = None created_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=datetime.utcnow)