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:
@@ -0,0 +1,117 @@
|
||||
"""Web search, research, and image tools (require SearXNG)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fabledassistant.services.tools._registry import tool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@tool(
|
||||
name="search_web",
|
||||
description=(
|
||||
"Search the web for quick information and answer the user's question from results. "
|
||||
"Use for factual lookups, current events, version numbers, quick definitions, or any question "
|
||||
"where a fast web answer is needed without creating a note. "
|
||||
"Use research_topic instead when the user explicitly wants a comprehensive note or deep research."
|
||||
),
|
||||
parameters={
|
||||
"query": {"type": "string", "description": "The search query"},
|
||||
},
|
||||
required=["query"],
|
||||
requires="searxng",
|
||||
)
|
||||
async def search_web_tool(*, user_id, arguments, **_ctx):
|
||||
from fabledassistant.services.research import _search_searxng
|
||||
|
||||
query = arguments.get("query", "")
|
||||
results = await _search_searxng(query)
|
||||
if not results:
|
||||
return {"success": False, "error": f"No results found for '{query}'"}
|
||||
return {
|
||||
"success": True,
|
||||
"type": "web_search",
|
||||
"data": {"query": query, "results": results, "count": len(results)},
|
||||
}
|
||||
|
||||
|
||||
@tool(
|
||||
name="research_topic",
|
||||
description=(
|
||||
"Research a topic by searching the web and compiling a note with cited sources. "
|
||||
"Use for 'research X', 'look up X', 'find information about X', "
|
||||
"'compile notes on X'. Takes 30–120 seconds."
|
||||
),
|
||||
parameters={
|
||||
"topic": {"type": "string", "description": "The topic or question to research"},
|
||||
},
|
||||
required=["topic"],
|
||||
requires="searxng",
|
||||
)
|
||||
async def research_topic_tool(*, user_id, arguments, **_ctx):
|
||||
# Research is always intercepted in generation_task.py (round 0) before execute_tool.
|
||||
# Reaching here indicates a code path regression.
|
||||
topic = arguments.get("topic", "")
|
||||
logger.error(
|
||||
"research_topic reached execute_tool — should have been intercepted upstream "
|
||||
"in generation_task.py. topic=%r",
|
||||
topic,
|
||||
)
|
||||
return {"success": False, "error": "Research could not be started. Please try again."}
|
||||
|
||||
|
||||
@tool(
|
||||
name="search_images",
|
||||
description="Search and display images inline. Use ONLY when user explicitly asks to see or show an image or photo. Not for factual questions.",
|
||||
parameters={
|
||||
"query": {"type": "string", "description": "The image search query"},
|
||||
},
|
||||
required=["query"],
|
||||
requires="searxng",
|
||||
)
|
||||
async def search_images_tool(*, user_id, arguments, **_ctx):
|
||||
from fabledassistant.services.images import fetch_and_store_image
|
||||
from fabledassistant.services.research import _search_searxng_images
|
||||
|
||||
query = arguments.get("query", "")
|
||||
if not query:
|
||||
return {"success": False, "error": "query is required"}
|
||||
raw_results = await _search_searxng_images(query)
|
||||
if not raw_results:
|
||||
return {"success": False, "error": f"No images found for '{query}'"}
|
||||
images = []
|
||||
for r in raw_results[:2]:
|
||||
img_url = r.get("img_src", "")
|
||||
if not img_url:
|
||||
continue
|
||||
record = await fetch_and_store_image(
|
||||
url=img_url,
|
||||
title=r.get("title"),
|
||||
source_domain=r.get("source_domain"),
|
||||
)
|
||||
if record:
|
||||
title = record.title or query
|
||||
page_url = r.get("page_url", "")
|
||||
source_domain = r.get("source_domain", "")
|
||||
images.append({
|
||||
"embed": f"",
|
||||
"citation": f"*Source: [{source_domain or 'image'}]({page_url})*",
|
||||
"page_url": page_url,
|
||||
"title": title,
|
||||
})
|
||||
if not images:
|
||||
return {"success": False, "error": f"Could not retrieve images for '{query}'"}
|
||||
return {
|
||||
"success": True,
|
||||
"type": "image_search",
|
||||
"data": {
|
||||
"query": query,
|
||||
"images": images,
|
||||
"instructions": (
|
||||
"Embed each image in your response by writing the 'embed' field verbatim, "
|
||||
"then the 'citation' field on the next line. Do not paraphrase or list URLs."
|
||||
),
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user