Data model: - Migration 0036: adds note_type TEXT (default 'note') and metadata JSONB to the notes table; index on note_type - Note model: entity_type property, note_type/metadata in to_dict() - create_note() accepts note_type and metadata params Backend: - /api/knowledge — unified paginated endpoint: type/tag/sort/q filters, semantic search via embeddings, excludes tasks - /api/knowledge/tags — distinct tags across knowledge objects - New LLM tools: create_person, create_place, create_list, add_to_list, clear_checked_items — all wired into execute_tool() - People and places auto-injected as compact summary into LLM system prompt Frontend: - KnowledgeView replaces HomeView at /; left filter panel (type+tag), toolbar (search, sort, graph toggle), card grid with type-aware cards (indigo=note, emerald=person, amber=place, sky=list), load-more pagination - Today bar: upcoming events, overdue task count, Briefing/Chat links - Floating mini-chat sticky to bottom: creates/continues a conversation inline, message history expands upward, close button ends session - Graph panel: toggles as a 420px right panel at full viewport width - AppHeader: Knowledge, Chat, Briefing, Calendar, Tasks, Projects - Router: / → KnowledgeView; /knowledge redirect; HomeView import removed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
104 lines
4.0 KiB
Python
104 lines
4.0 KiB
Python
import enum
|
|
from datetime import date, datetime
|
|
|
|
from sqlalchemy import Date, DateTime, ForeignKey, Index, Integer, Text
|
|
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from fabledassistant.models import Base
|
|
from fabledassistant.models.base import TimestampMixin
|
|
|
|
|
|
class TaskStatus(str, enum.Enum):
|
|
todo = "todo"
|
|
in_progress = "in_progress"
|
|
done = "done"
|
|
cancelled = "cancelled"
|
|
|
|
|
|
class TaskPriority(str, enum.Enum):
|
|
none = "none"
|
|
low = "low"
|
|
medium = "medium"
|
|
high = "high"
|
|
|
|
|
|
class Note(Base, TimestampMixin):
|
|
__tablename__ = "notes"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
user_id: Mapped[int | None] = mapped_column(
|
|
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True
|
|
)
|
|
title: Mapped[str] = mapped_column(Text, default="")
|
|
body: Mapped[str] = mapped_column(Text, default="")
|
|
tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list)
|
|
parent_id: Mapped[int | None] = mapped_column(
|
|
Integer, ForeignKey("notes.id", ondelete="SET NULL"), nullable=True
|
|
)
|
|
project_id: Mapped[int | None] = mapped_column(
|
|
Integer, ForeignKey("projects.id", ondelete="SET NULL"), nullable=True
|
|
)
|
|
milestone_id: Mapped[int | None] = mapped_column(
|
|
Integer, ForeignKey("milestones.id", ondelete="SET NULL"), nullable=True
|
|
)
|
|
status: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
priority: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
due_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
|
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
recurrence_rule: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
recurrence_next_spawn_at: Mapped[datetime | None] = mapped_column(
|
|
DateTime(timezone=True), nullable=True
|
|
)
|
|
# Entity type — 'note' (default), 'person', 'place', 'list'
|
|
note_type: Mapped[str] = mapped_column(Text, default="note", server_default="note")
|
|
# Structured metadata for entity types (person/place/list)
|
|
metadata: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
|
|
|
__table_args__ = (
|
|
Index("ix_notes_tags", "tags", postgresql_using="gin"),
|
|
Index("ix_notes_status", "status"),
|
|
Index("ix_notes_title", "title"),
|
|
Index("ix_notes_user_id", "user_id"),
|
|
Index("ix_notes_project_id", "project_id"),
|
|
Index("ix_notes_milestone_id", "milestone_id"),
|
|
Index("ix_notes_note_type", "note_type"),
|
|
)
|
|
|
|
@property
|
|
def is_task(self) -> bool:
|
|
return self.status is not None
|
|
|
|
@property
|
|
def entity_type(self) -> str:
|
|
"""Normalised type: 'note', 'person', 'place', or 'list'."""
|
|
return self.note_type or "note"
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"id": self.id,
|
|
"title": self.title,
|
|
"body": self.body,
|
|
"tags": self.tags or [],
|
|
"parent_id": self.parent_id,
|
|
"project_id": self.project_id,
|
|
"milestone_id": self.milestone_id,
|
|
"status": self.status,
|
|
"priority": self.priority,
|
|
"due_date": self.due_date.isoformat() if self.due_date else None,
|
|
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
|
"recurrence_rule": self.recurrence_rule,
|
|
"recurrence_next_spawn_at": (
|
|
self.recurrence_next_spawn_at.isoformat()
|
|
if self.recurrence_next_spawn_at
|
|
else None
|
|
),
|
|
"is_task": self.is_task,
|
|
"note_type": self.entity_type,
|
|
"metadata": self.metadata or {},
|
|
"created_at": self.created_at.isoformat(),
|
|
"updated_at": self.updated_at.isoformat(),
|
|
}
|