From 82b79ef5d0c7ad384c3d471e53b9f1e959f25a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis-F=C3=A9lix=20Nothias?= Date: Fri, 10 Jul 2026 09:36:47 +0200 Subject: [PATCH 1/2] fix(tests): look through FastAPI's include_router wrapper when listing routes --- tests/unit/test_web_app_routes.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_web_app_routes.py b/tests/unit/test_web_app_routes.py index 0957cd5b..a454dfab 100644 --- a/tests/unit/test_web_app_routes.py +++ b/tests/unit/test_web_app_routes.py @@ -54,12 +54,27 @@ def _load_app(): return app +def _flatten_routes(routes): + """Yield leaf routes, descending into routers wrapped by include_router. + + FastAPI >= 0.139 no longer copies an included router's routes onto the app. + It appends one wrapper per include_router() call, exposing the router as + ``original_router``. Older versions flatten, leaving nothing to descend into. + """ + for route in routes: + included_router = getattr(route, "original_router", None) + if included_router is None: + yield route + else: + yield from _flatten_routes(included_router.routes) + + def _route_methods_by_path(app): - """Return {path: {methods}} for every APIRoute on the app.""" + """Return {path: {methods}} for every APIRoute reachable from the app.""" from fastapi.routing import APIRoute out: dict[str, set[str]] = {} - for r in app.routes: + for r in _flatten_routes(app.routes): if isinstance(r, APIRoute): out.setdefault(r.path, set()).update(r.methods or set()) else: From 49f1cf00a6383baef0b338e18bfe6ce37bf5b7ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis-F=C3=A9lix=20Nothias?= Date: Fri, 10 Jul 2026 09:36:47 +0200 Subject: [PATCH 2/2] fix(mcp): document ensure_kb and ground_paper in the usage guide --- src/perspicacite/mcp/usage_guide.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/perspicacite/mcp/usage_guide.py b/src/perspicacite/mcp/usage_guide.py index 71293e3f..ce52f2d3 100644 --- a/src/perspicacite/mcp/usage_guide.py +++ b/src/perspicacite/mcp/usage_guide.py @@ -171,6 +171,30 @@ "when_to_use": "Ingest specific DOIs into a KB.", "key_knobs": [], }, + { + "name": "ensure_kb", + "purpose": ( + "Idempotently create and ingest a per-paper KB for a DOI, using the ASB " + "binder slug convention. Returns immediately if the KB already has chunks." + ), + "when_to_use": ( + "Before grounding questions against a single paper, to guarantee its KB " + "exists without re-ingesting it on every call." + ), + "key_knobs": ["doi (required)", "mode — reserved, currently unused"], + }, + { + "name": "ground_paper", + "purpose": ( + "Answer a research question against one paper's dedicated KB, ensuring " + "that KB exists first (via ensure_kb)." + ), + "when_to_use": ( + "Ask a question of a specific paper by DOI in one call. Set tier='si' to " + "prefer evidence from the paper's supplementary information." + ), + "key_knobs": ["doi (required)", "question (required)", "tier (paper|si) — default: paper"], + }, { "name": "ingest_local_documents", "purpose": "Ingest local files (PDF/text) into a KB.",