"""SQLAlchemy models for Project Memory.""" from sqlalchemy import Column, String, DateTime, ForeignKey, Text, Enum, JSON, Boolean from sqlalchemy.orm import relationship from datetime import datetime import uuid import enum import random from app.database import Base # Special user ID for AI Agent AI_AGENT_USER_ID = "ai-agent" def generate_uuid() -> str: """Generate a new UUID string.""" return str(uuid.uuid4()) def generate_user_id(first_name: str) -> str: """Generate user ID: first 3 letters of firstname + 4 random digits. Example: 'Amal' -> 'ama1234' """ prefix = first_name[:3].lower() suffix = ''.join([str(random.randint(0, 9)) for _ in range(4)]) return f"{prefix}{suffix}" class ActorType(str, enum.Enum): """Who performed the action.""" human = "human" agent = "agent" class ActionType(str, enum.Enum): """Type of action recorded.""" task_completed = "task_completed" doc_generated = "doc_generated" query_answered = "query_answered" class TaskStatus(str, enum.Enum): """Task status states.""" todo = "todo" in_progress = "in_progress" done = "done" class User(Base): """User account.""" __tablename__ = "users" id = Column(String, primary_key=True) # Generated as first_name[:3] + 4 random digits first_name = Column(String, nullable=False) last_name = Column(String, nullable=False) avatar_url = Column(String, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) # Relationships memberships = relationship("ProjectMembership", back_populates="user") created_projects = relationship("Project", back_populates="creator") log_entries = relationship("LogEntry", back_populates="user") @property def name(self) -> str: """Full name for backward compatibility.""" return f"{self.first_name} {self.last_name}" class Project(Base): """Project that contains tasks and memory. Option A: project name is also the stable project ID. We persist the ID explicitly from the tools layer (create_project uses the provided name as id). """ __tablename__ = "projects" # ID is provided by callers (equal to the project name); no UUID default id = Column(String, primary_key=True) name = Column(String, nullable=False) description = Column(Text, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) created_by = Column(String, ForeignKey("users.id"), nullable=True) agent_enabled = Column(Boolean, default=False) # Whether AI agent is enabled for this project # Relationships creator = relationship("User", back_populates="created_projects") memberships = relationship("ProjectMembership", back_populates="project") tasks = relationship("Task", back_populates="project") log_entries = relationship("LogEntry", back_populates="project") class ProjectMembership(Base): """Association between users and projects.""" __tablename__ = "project_memberships" id = Column(String, primary_key=True, default=generate_uuid) project_id = Column(String, ForeignKey("projects.id"), nullable=False) user_id = Column(String, ForeignKey("users.id"), nullable=False) role = Column(String, default="member") # "owner" or "member" joined_at = Column(DateTime, default=datetime.utcnow) # Relationships project = relationship("Project", back_populates="memberships") user = relationship("User", back_populates="memberships") class Task(Base): """Task within a project.""" __tablename__ = "tasks" id = Column(String, primary_key=True, default=generate_uuid) project_id = Column(String, ForeignKey("projects.id"), nullable=False) title = Column(String, nullable=False) description = Column(Text, nullable=True) status = Column(Enum(TaskStatus), default=TaskStatus.todo) assigned_to = Column(String, nullable=True) # userId or "agent" working_by = Column(String, nullable=True) # User ID currently working on this task created_at = Column(DateTime, default=datetime.utcnow) completed_at = Column(DateTime, nullable=True) # Relationships project = relationship("Project", back_populates="tasks") log_entries = relationship("LogEntry", back_populates="task") class LogEntry(Base): """ The core of project memory. Records what was done, by whom, and stores LLM-generated documentation. """ __tablename__ = "log_entries" id = Column(String, primary_key=True, default=generate_uuid) project_id = Column(String, ForeignKey("projects.id"), nullable=False) task_id = Column(String, ForeignKey("tasks.id"), nullable=True) user_id = Column(String, ForeignKey("users.id"), nullable=True) actor_type = Column(Enum(ActorType), nullable=False) action_type = Column(Enum(ActionType), nullable=False) raw_input = Column(Text, nullable=False) # What user typed code_snippet = Column(Text, nullable=True) # Optional code generated_doc = Column(Text, nullable=False) # LLM-generated documentation tags = Column(JSON, default=list) # Extracted tags created_at = Column(DateTime, default=datetime.utcnow) # Relationships project = relationship("Project", back_populates="log_entries") task = relationship("Task", back_populates="log_entries") user = relationship("User", back_populates="log_entries")