From dd16b39218c9f90bf48f7eb2ff4c5573961bff64 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 8 Apr 2026 10:27:31 -0400 Subject: [PATCH] feat(knowledge): include tasks in knowledge queries and counts --- src/fabledassistant/routes/knowledge.py | 2 +- src/fabledassistant/services/knowledge.py | 76 +++++++++++++++-------- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/fabledassistant/routes/knowledge.py b/src/fabledassistant/routes/knowledge.py index 5cc694b..9dfef6b 100644 --- a/src/fabledassistant/routes/knowledge.py +++ b/src/fabledassistant/routes/knowledge.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) knowledge_bp = Blueprint("knowledge", __name__, url_prefix="/api/knowledge") -_VALID_TYPES = {"note", "person", "place", "list"} +_VALID_TYPES = {"note", "person", "place", "list", "task"} _VALID_SORTS = {"modified", "created", "alpha", "type"} diff --git a/src/fabledassistant/services/knowledge.py b/src/fabledassistant/services/knowledge.py index 5c586b9..800f722 100644 --- a/src/fabledassistant/services/knowledge.py +++ b/src/fabledassistant/services/knowledge.py @@ -46,6 +46,14 @@ def _note_to_item(note: Note) -> dict: item["item_count"] = len(list_items) item["checked_count"] = sum(1 for i in list_items if i["checked"]) item["body"] = body + + # Task fields — override note_type and add status/priority/due_date + if note.is_task: + item["note_type"] = "task" + item["status"] = note.status + item["priority"] = note.priority + item["due_date"] = note.due_date.isoformat() if note.due_date else None + return item @@ -69,17 +77,15 @@ async def query_knowledge( ) async with async_session() as session: - base = ( - select(Note) - .where(Note.user_id == user_id) - .where(Note.status.is_(None)) # exclude tasks - ) + base = select(Note).where(Note.user_id == user_id) - if note_type: - base = base.where(Note.note_type == note_type) + if note_type == "task": + base = base.where(Note.status.isnot(None)) + elif note_type: + base = base.where(Note.note_type == note_type).where(Note.status.is_(None)) else: - # Exclude tasks — already done above; also exclude any legacy nulls - base = base.where(Note.note_type.in_(["note", "person", "place", "list"])) + # All types including tasks + pass for tag in tags: base = base.where(Note.tags.contains([tag])) @@ -115,12 +121,13 @@ async def _semantic_knowledge_search( try: from fabledassistant.services.embeddings import semantic_search_notes # Fetch a larger candidate set to allow for filtering + is_task_filter = True if note_type == "task" else (False if note_type else None) candidates = await semantic_search_notes( user_id=user_id, query=q, limit=min(200, limit * 8), threshold=0.3, - is_task=False, + is_task=is_task_filter, ) except Exception: logger.warning("Semantic search unavailable, falling back to SQL", exc_info=True) @@ -128,7 +135,9 @@ async def _semantic_knowledge_search( results = [] for _score, note in candidates: - if note_type and note.entity_type != note_type: + if note_type == "task" and not note.is_task: + continue + elif note_type and note_type != "task" and note.entity_type != note_type: continue if tags and not all(t in (note.tags or []) for t in tags): continue @@ -145,12 +154,13 @@ async def get_knowledge_tags(user_id: int, note_type: str | None = None) -> list base = ( select(func.unnest(Note.tags).label("tag")) .where(Note.user_id == user_id) - .where(Note.status.is_(None)) ) - if note_type: - base = base.where(Note.note_type == note_type) + if note_type == "task": + base = base.where(Note.status.isnot(None)) + elif note_type: + base = base.where(Note.note_type == note_type).where(Note.status.is_(None)) else: - base = base.where(Note.note_type.in_(["note", "person", "place", "list"])) + pass stmt = base.distinct().order_by("tag") rows = list((await session.execute(stmt)).scalars().all()) return [r for r in rows if r] @@ -159,6 +169,7 @@ async def get_knowledge_tags(user_id: int, note_type: str | None = None) -> list async def get_knowledge_counts(user_id: int, tags: list[str] | None = None) -> dict[str, int]: """Return per-type count of knowledge objects for the sidebar display.""" async with async_session() as session: + # Count non-task types stmt = ( select(Note.note_type, func.count(Note.id)) .where(Note.user_id == user_id) @@ -170,11 +181,23 @@ async def get_knowledge_counts(user_id: int, tags: list[str] | None = None) -> d for tag in tags: stmt = stmt.where(Note.tags.contains([tag])) rows = list((await session.execute(stmt)).all()) - counts = {row[0]: row[1] for row in rows} - # Ensure all types present even if zero - for t in ("note", "person", "place", "list"): + counts = {row[0]: row[1] for row in rows} + + # Count tasks separately (is_task = status IS NOT NULL) + task_stmt = ( + select(func.count(Note.id)) + .where(Note.user_id == user_id) + .where(Note.status.isnot(None)) + ) + if tags: + for tag in tags: + task_stmt = task_stmt.where(Note.tags.contains([tag])) + task_count: int = (await session.execute(task_stmt)).scalar_one() + counts["task"] = task_count + + for t in ("note", "person", "place", "list", "task"): counts.setdefault(t, 0) - counts["total"] = sum(counts[t] for t in ("note", "person", "place", "list")) + counts["total"] = sum(counts[t] for t in ("note", "person", "place", "list", "task")) return counts @@ -197,15 +220,14 @@ async def query_knowledge_ids( return [item["id"] for item in items], total async with async_session() as session: - base = ( - select(Note.id) - .where(Note.user_id == user_id) - .where(Note.status.is_(None)) - ) - if note_type: - base = base.where(Note.note_type == note_type) + base = select(Note.id).where(Note.user_id == user_id) + + if note_type == "task": + base = base.where(Note.status.isnot(None)) + elif note_type: + base = base.where(Note.note_type == note_type).where(Note.status.is_(None)) else: - base = base.where(Note.note_type.in_(["note", "person", "place", "list"])) + pass for tag in tags: base = base.where(Note.tags.contains([tag]))