Files
platform/services/brain/app/models/taxonomy.py
Yusuf Suleman 68a8d4c228 feat: brain taxonomy — DB-backed folders/tags, sidebar, CRUD API
Backend:
- New Folder/Tag/ItemTag models with proper relational tables
- Taxonomy CRUD endpoints: list, create, rename, delete, merge tags
- Sidebar endpoint with folder/tag counts
- AI classification reads live folders/tags from DB, not hardcoded
- Default folders/tags seeded on first request per user
- folder_id FK on items for relational integrity

Frontend:
- Left sidebar with Folders/Tags tabs (like Karakeep)
- Click folder/tag to filter items
- "Manage" mode: add new folders/tags, delete existing
- Counts next to each folder/tag
- "All items" option to clear filter
- Replaces the old signal-strip cards

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 20:23:45 -05:00

104 lines
3.5 KiB
Python

"""Database models for folders and tags — editable taxonomy."""
import re
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, Index, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from app.database import Base
def new_id():
return str(uuid.uuid4())
def slugify(text: str) -> str:
s = text.lower().strip()
s = re.sub(r'[^\w\s-]', '', s)
s = re.sub(r'[\s_]+', '-', s)
return s.strip('-')
class Folder(Base):
__tablename__ = "folders"
id = Column(UUID(as_uuid=False), primary_key=True, default=new_id)
user_id = Column(String(64), nullable=False, index=True)
name = Column(String(128), nullable=False)
slug = Column(String(128), nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
sort_order = Column(Integer, default=0, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
__table_args__ = (
UniqueConstraint('user_id', 'slug', name='uq_folder_user_slug'),
)
class Tag(Base):
__tablename__ = "tags"
id = Column(UUID(as_uuid=False), primary_key=True, default=new_id)
user_id = Column(String(64), nullable=False, index=True)
name = Column(String(128), nullable=False)
slug = Column(String(128), nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
sort_order = Column(Integer, default=0, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
__table_args__ = (
UniqueConstraint('user_id', 'slug', name='uq_tag_user_slug'),
)
class ItemTag(Base):
__tablename__ = "item_tags"
item_id = Column(UUID(as_uuid=False), ForeignKey("items.id", ondelete="CASCADE"), primary_key=True)
tag_id = Column(UUID(as_uuid=False), ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# Default folders to seed for new users
DEFAULT_FOLDERS = ["Home", "Family", "Work", "Travel", "Knowledge", "Faith", "Projects"]
# Default tags to seed for new users
DEFAULT_TAGS = [
"reference", "important", "legal", "financial", "insurance",
"research", "idea", "guide", "tutorial", "setup", "how-to",
"tools", "dev", "server", "selfhosted", "home-assistant",
"shopping", "compare", "buy", "product",
"family", "kids", "health", "travel", "faith",
"video", "read-later", "books",
]
async def ensure_user_taxonomy(db, user_id: str):
"""Seed default folders and tags for a new user if they have none."""
from sqlalchemy import select, func
folder_count = (await db.execute(
select(func.count()).where(Folder.user_id == user_id)
)).scalar() or 0
if folder_count == 0:
for i, name in enumerate(DEFAULT_FOLDERS):
db.add(Folder(id=new_id(), user_id=user_id, name=name, slug=slugify(name), sort_order=i))
await db.flush()
tag_count = (await db.execute(
select(func.count()).where(Tag.user_id == user_id)
)).scalar() or 0
if tag_count == 0:
for i, name in enumerate(DEFAULT_TAGS):
db.add(Tag(id=new_id(), user_id=user_id, name=name, slug=slugify(name), sort_order=i))
await db.flush()
await db.commit()