"""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=( "Quick web lookup — returns search result snippets for factual questions, current events, " "definitions, or version numbers. No note is saved. Use research_topic when the user wants " "a comprehensive written report saved as a note." ), 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=( "Deep web research — searches multiple sources, synthesizes findings, and saves the result " "as a structured note with sections and citations. Use when the user says 'research', " "'look into', or wants a comprehensive write-up. Takes 30–120 seconds. " "For a quick factual answer without saving a note, use search_web." ), 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 the user explicitly asks to see, show, or find an image or photo. Not for factual questions — use search_web for those.", 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"![{title}](/api/images/{record.id})", "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." ), }, }