refactor(tools): decorator-based tool registry replaces monolithic tools.py

Split 2566-line tools.py into a tools/ package with @tool decorator
registration. Each tool's schema, metadata, and implementation live
together. Briefing eligibility is now a briefing=True flag instead of
a separate frozenset allowlist. Conditional inclusion (CalDAV, SearXNG)
uses requires= metadata. Public API (get_tools_for_user, execute_tool)
unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-12 10:01:00 -04:00
parent 5275be8588
commit ce2d76447c
15 changed files with 2129 additions and 2624 deletions
+5 -58
View File
@@ -1,62 +1,9 @@
"""
Curated read-only tool subset for agentic briefings.
"""Briefing tool subset — delegates to the registry's ``briefing=True`` filter.
The main chat pipeline exposes 40+ tools via ``tools.get_tools_for_user``,
including mutating tools (``create_task``, ``delete_note``) and
external-search tools (``search_images``, ``search_web``). Neither is
appropriate for a scheduled background job that generates briefings —
briefings are read-only and should not reach out to the internet on the
user's behalf, and leaving high-noise tools in the list increases the
chance of spurious calls (e.g. ``search_images`` firing on "what
meeting?").
This module maintains an explicit allowlist. New tools added to
``tools.py`` are not automatically exposed to briefings — they must be
opted in by name here.
Tools are opted-in to briefings via ``@tool(briefing=True)`` in their
respective module, so there is no separate allowlist to maintain here.
"""
import logging
from fabledassistant.services.tools import get_briefing_tools
from fabledassistant.services.tools import get_tools_for_user
logger = logging.getLogger(__name__)
# Explicit allowlist — tools a briefing is permitted to call. Read-only only.
# Any tool not listed here is invisible to the briefing model.
BRIEFING_TOOL_NAMES: frozenset[str] = frozenset({
# Tasks — what's actionable today, overdue, or high priority
"list_tasks",
# Calendar — internal event store and CalDAV (if configured)
"list_events",
"search_events",
# Weather — today's forecast for the user's configured locations
"get_weather",
# News — RSS items filtered by user preferences
"get_rss_items",
# Projects — context for prioritization and narrative continuity
"list_projects",
"search_projects",
"get_project",
# Notes — surface recent captures for "pick up where you left off"
"list_notes",
"get_note",
})
async def get_briefing_tools(user_id: int) -> list[dict]:
"""Return the tool schemas a briefing run is permitted to call.
Builds the user's full tool list (so user-specific gating such as
CalDAV availability still applies) and filters it down to the
briefing allowlist.
"""
all_tools = await get_tools_for_user(user_id)
filtered = [
t for t in all_tools
if t.get("function", {}).get("name") in BRIEFING_TOOL_NAMES
]
logger.debug(
"Briefing tools for user %d: %d of %d selected",
user_id, len(filtered), len(all_tools),
)
return filtered
__all__ = ["get_briefing_tools"]