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>
This commit is contained in:
@@ -159,18 +159,53 @@ async def _process_item(item_id: str):
|
||||
if result.get("page_count"):
|
||||
item.metadata_json["page_count"] = result["page_count"]
|
||||
|
||||
# ── Step 2: AI classification ──
|
||||
log.info(f"Classifying item {item.id}")
|
||||
# ── Step 2: Fetch live taxonomy from DB ──
|
||||
from app.models.taxonomy import Folder as FolderModel, Tag as TagModel, ItemTag, ensure_user_taxonomy
|
||||
await ensure_user_taxonomy(db, item.user_id)
|
||||
|
||||
active_folders = (await db.execute(
|
||||
select(FolderModel).where(FolderModel.user_id == item.user_id, FolderModel.is_active == True)
|
||||
.order_by(FolderModel.sort_order)
|
||||
)).scalars().all()
|
||||
active_tags = (await db.execute(
|
||||
select(TagModel).where(TagModel.user_id == item.user_id, TagModel.is_active == True)
|
||||
.order_by(TagModel.sort_order)
|
||||
)).scalars().all()
|
||||
|
||||
folder_names = [f.name for f in active_folders]
|
||||
tag_names = [t.name for t in active_tags]
|
||||
folder_map = {f.name: f for f in active_folders}
|
||||
tag_map = {t.name: t for t in active_tags}
|
||||
|
||||
# ── Step 3: AI classification ──
|
||||
log.info(f"Classifying item {item.id} with {len(folder_names)} folders, {len(tag_names)} tags")
|
||||
classification = await classify_item(
|
||||
item_type=item.type,
|
||||
url=item.url,
|
||||
title=title,
|
||||
text=extracted_text,
|
||||
folders=folder_names,
|
||||
tags=tag_names,
|
||||
)
|
||||
|
||||
item.title = classification.get("title") or title or "Untitled"
|
||||
item.folder = classification.get("folder", "Knowledge")
|
||||
item.tags = classification.get("tags", ["reference", "read-later"])
|
||||
|
||||
# Set folder (relational + denormalized)
|
||||
classified_folder = classification.get("folder", folder_names[0] if folder_names else "Knowledge")
|
||||
item.folder = classified_folder
|
||||
if classified_folder in folder_map:
|
||||
item.folder_id = folder_map[classified_folder].id
|
||||
|
||||
# Set tags (relational + denormalized)
|
||||
classified_tags = classification.get("tags", [])
|
||||
item.tags = classified_tags
|
||||
|
||||
# Clear old item_tags and create new ones
|
||||
from sqlalchemy import delete as sa_delete
|
||||
await db.execute(sa_delete(ItemTag).where(ItemTag.item_id == item.id))
|
||||
for tag_name in classified_tags:
|
||||
if tag_name in tag_map:
|
||||
db.add(ItemTag(item_id=item.id, tag_id=tag_map[tag_name].id))
|
||||
|
||||
# For notes: replace raw_content with spell-corrected version
|
||||
corrected = classification.get("corrected_text", "")
|
||||
|
||||
Reference in New Issue
Block a user