refactor: rename package fabledassistant -> scribe (code-only)
Renames src/fabledassistant -> src/scribe and all imports, plus the default DB name and DB user/password (fabled -> scribe) in config + compose. 952 refs / 154 files. Reverses the old 'internal name stays fabledassistant' convention. Code-only: live databases are still physically named 'fabledassistant'. Deployed environments must set POSTGRES_DB / POSTGRES_USER (or rename the DB) since the defaults now resolve to 'scribe'. Repo (FabledScribe), git host (fabledsword), MCP (fabled-git) and the image name (fabledscribe) are intentionally unchanged. ruff check src/ clean locally; CI (typecheck + pytest) is the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""Unified Knowledge endpoint — notes, people, places, lists in one queryable feed."""
|
||||
import logging
|
||||
|
||||
from quart import Blueprint, jsonify, request
|
||||
|
||||
from scribe.auth import get_current_user_id, login_required
|
||||
from scribe.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", "plan", "process"}
|
||||
_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 scribe.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 scribe.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 scribe.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 scribe.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 scribe.services.knowledge import get_knowledge_counts as _counts
|
||||
counts = await _counts(uid, tags=tags)
|
||||
return jsonify(counts)
|
||||
Reference in New Issue
Block a user