feat(knowledge): include tasks in knowledge queries and counts

This commit is contained in:
2026-04-08 10:27:31 -04:00
parent 0bd362f582
commit dd16b39218
2 changed files with 50 additions and 28 deletions
+1 -1
View File
@@ -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"}
+49 -27
View File
@@ -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]))