"""Unified Knowledge endpoint — notes, people, places, lists in one queryable feed.""" import logging from quart import Blueprint, jsonify, request from fabledassistant.auth import get_current_user_id, login_required from fabledassistant.routes.utils import parse_pagination logger = logging.getLogger(__name__) knowledge_bp = Blueprint("knowledge", __name__, url_prefix="/api/knowledge") _VALID_TYPES = {"note", "person", "place", "list", "task"} _VALID_SORTS = {"modified", "created", "alpha", "type"} @knowledge_bp.route("", methods=["GET"]) @login_required async def list_knowledge(): """Return paginated knowledge objects with optional filtering. Query params: type — one of note|person|place|list (omit for all, excludes tasks) tags — comma-separated tag filter (AND logic) sort — modified|created|alpha|type (default: modified) q — search query (semantic when provided, keyword fallback) page — 1-based page number (default 1) per_page — items per page (default 24, max 100) """ uid = get_current_user_id() note_type = request.args.get("type", "").strip().lower() or None tags_raw = request.args.get("tags", "").strip() tags = [t.strip() for t in tags_raw.split(",") if t.strip()] if tags_raw else [] sort = request.args.get("sort", "modified").strip().lower() q = request.args.get("q", "").strip() or None if note_type and note_type not in _VALID_TYPES: return jsonify({"error": f"Invalid type. Must be one of: {', '.join(sorted(_VALID_TYPES))}"}), 400 if sort not in _VALID_SORTS: sort = "modified" limit, offset = parse_pagination(default_limit=24, max_limit=100) page = max(1, int(request.args.get("page", 1))) from fabledassistant.services.knowledge import query_knowledge items, total = await query_knowledge( user_id=uid, note_type=note_type, tags=tags, sort=sort, q=q, limit=limit, offset=offset, ) return jsonify({ "items": items, "total": total, "page": page, "per_page": limit, "pages": max(1, (total + limit - 1) // limit), }) @knowledge_bp.route("/ids", methods=["GET"]) @login_required async def list_knowledge_ids(): """Return note IDs only (cheap) for the two-tier pagination feed. Same filter params as GET /api/knowledge. Additional params: limit (default 100, max 200), offset (default 0). Returns {ids, total, has_more}. """ uid = get_current_user_id() note_type = request.args.get("type", "").strip().lower() or None tags_raw = request.args.get("tags", "").strip() tags = [t.strip() for t in tags_raw.split(",") if t.strip()] if tags_raw else [] sort = request.args.get("sort", "modified").strip().lower() q = request.args.get("q", "").strip() or None if sort not in _VALID_SORTS: sort = "modified" try: limit = min(int(request.args.get("limit", 100)), 200) offset = max(0, int(request.args.get("offset", 0))) except ValueError: return jsonify({"error": "Invalid limit or offset"}), 400 if note_type and note_type not in _VALID_TYPES: return jsonify({"error": "Invalid type"}), 400 from fabledassistant.services.knowledge import query_knowledge_ids ids, total = await query_knowledge_ids( user_id=uid, note_type=note_type, tags=tags, sort=sort, q=q, limit=limit, offset=offset, ) return jsonify({"ids": ids, "total": total, "has_more": (offset + len(ids)) < total}) @knowledge_bp.route("/batch", methods=["GET"]) @login_required async def get_knowledge_batch(): """Fetch full items for a comma-separated list of IDs (max 100). Returns {items: [...]} in the order of the requested IDs. """ uid = get_current_user_id() ids_raw = request.args.get("ids", "").strip() if not ids_raw: return jsonify({"items": []}) try: ids = [int(x) for x in ids_raw.split(",") if x.strip()] except ValueError: return jsonify({"error": "Invalid IDs"}), 400 if len(ids) > 100: return jsonify({"error": "Too many IDs (max 100)"}), 400 from fabledassistant.services.knowledge import get_knowledge_by_ids items = await get_knowledge_by_ids(uid, ids) return jsonify({"items": items}) @knowledge_bp.route("/tags", methods=["GET"]) @login_required async def list_knowledge_tags(): """Return all tags used across knowledge objects (excludes tasks).""" uid = get_current_user_id() note_type = request.args.get("type", "").strip().lower() or None from fabledassistant.services.knowledge import get_knowledge_tags tags = await get_knowledge_tags(uid, note_type=note_type) return jsonify({"tags": tags}) @knowledge_bp.route("/counts", methods=["GET"]) @login_required async def get_knowledge_counts(): """Return per-type counts — used by the sidebar to show item counts.""" uid = get_current_user_id() tags_raw = request.args.get("tags", "").strip() tags = [t.strip() for t in tags_raw.split(",") if t.strip()] if tags_raw else None from fabledassistant.services.knowledge import get_knowledge_counts as _counts counts = await _counts(uid, tags=tags) return jsonify(counts)