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
+63
View File
@@ -0,0 +1,63 @@
"""RAG scope and calculation tools."""
from __future__ import annotations
from fabledassistant.services.tools._registry import tool
@tool(
name="set_rag_scope",
description="Change the RAG scope for this conversation. Use project_id=<int> to scope to a project, project_id=null to scope to orphan notes only, project_id=-1 for all notes.",
parameters={
"project_id": {"type": ["integer", "null"], "description": "Project ID to scope to, null for orphan-only, -1 for all notes"},
},
required=["project_id"],
)
async def set_rag_scope_tool(*, user_id, arguments, conv_id=None, workspace_project_id=None, **_ctx):
if workspace_project_id is not None:
return {"success": False, "error": "Cannot change RAG scope in workspace view"}
if conv_id is None:
return {"success": False, "error": "No conversation context available"}
project_id = arguments.get("project_id")
if project_id is not None and project_id != -1:
from fabledassistant.services.projects import get_project
proj = await get_project(user_id, int(project_id))
if proj is None:
return {"success": False, "error": "Project not found"}
scope_label = proj.title
elif project_id == -1:
scope_label = "All notes"
else:
scope_label = "Orphan notes only"
from fabledassistant.services.chat import update_conversation
await update_conversation(user_id, conv_id, rag_project_id=project_id)
return {"success": True, "type": "rag_scope_set", "scope_label": scope_label}
@tool(
name="calculate",
description=(
"Evaluate a mathematical expression and return the exact result. Use this for any "
"arithmetic, percentages, unit conversions, or multi-step calculations where precision "
"matters. Supports standard math operators (+, -, *, /, **, %) and all Python math "
"module functions (sqrt, log, sin, cos, floor, ceil, etc.)."
),
parameters={
"expression": {"type": "string", "description": "A valid Python math expression (e.g. '(450 * 1.13) / 12', 'sqrt(144)', 'log(1000, 10)')"},
},
required=["expression"],
)
async def calculate_tool(*, user_id, arguments, **_ctx):
import math as _math
expr = str(arguments.get("expression", "")).strip()
if not expr:
return {"success": False, "error": "expression is required"}
allowed_names = {k: v for k, v in vars(_math).items() if not k.startswith("_")}
allowed_names["abs"] = abs
allowed_names["round"] = round
try:
result = eval(expr, {"__builtins__": {}}, allowed_names) # noqa: S307
except Exception as calc_err:
return {"success": False, "error": f"Could not evaluate expression: {calc_err}"}
return {"success": True, "type": "calculation", "data": {"expression": expr, "result": result}}