File size: 5,464 Bytes
35765b5
 
698b2c1
35765b5
 
 
 
 
 
 
 
 
698b2c1
 
 
 
35765b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698b2c1
 
35765b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698b2c1
35765b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""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")