feat: replace search_web with unified lookup tool (Wikipedia + SearXNG fallback)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 21:56:31 -04:00
parent 6a8f0e9143
commit 06cd3493fd
2 changed files with 118 additions and 16 deletions
+57 -16
View File
@@ -1,4 +1,4 @@
"""Web search, research, and image tools (require SearXNG)."""
"""Web search, research, and image tools."""
from __future__ import annotations
@@ -10,29 +10,70 @@ logger = logging.getLogger(__name__)
@tool(
name="search_web",
name="lookup",
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."
"Look up a topic, concept, or factual question. Returns a concise answer from "
"Wikipedia or web sources. Use for definitions, explanations, 'what is X', "
"'how does Y work', current events, or version numbers. No note is saved. "
"For comprehensive written reports saved as notes, use research_topic instead."
),
parameters={
"query": {"type": "string", "description": "The search query"},
"query": {"type": "string", "description": "The topic or question to look up"},
},
required=["query"],
requires="searxng",
)
async def search_web_tool(*, user_id, arguments, **_ctx):
from fabledassistant.services.research import _search_searxng
async def lookup_tool(*, user_id, arguments, **_ctx):
from fabledassistant.config import Config
from fabledassistant.services.wikipedia import wiki_summary
query = arguments.get("query", "")
results = await _search_searxng(query)
if not results:
return {"success": False, "error": f"No results found for '{query}'"}
# 1. Try Wikipedia first
wiki = await wiki_summary(query)
if wiki:
return {
"success": True,
"type": "lookup",
"source": "wikipedia",
"data": wiki,
}
# 2. Fall back to SearXNG + article fetch
if Config.searxng_enabled():
from fabledassistant.services.research import _search_searxng
from fabledassistant.services.rss import _fetch_full_article
results = await _search_searxng(query)
if results:
articles: list[dict] = []
for r in results[:2]:
url = r.get("url", "")
if not url:
continue
content = await _fetch_full_article(url)
articles.append({
"url": url,
"title": r.get("title", url),
"snippet": r.get("snippet", ""),
"content": (content or "")[:4000],
})
if articles:
return {
"success": True,
"type": "lookup",
"source": "web",
"data": {"query": query, "results": articles},
}
# 3. No sources available
return {
"success": True,
"type": "web_search",
"data": {"query": query, "results": results, "count": len(results)},
"type": "lookup",
"source": "none",
"data": {
"query": query,
"message": "No results found. You can answer from your own knowledge.",
},
}
@@ -42,7 +83,7 @@ async def search_web_tool(*, user_id, arguments, **_ctx):
"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 30120 seconds. "
"For a quick factual answer without saving a note, use search_web."
"For a quick factual answer without saving a note, use lookup."
),
parameters={
"topic": {"type": "string", "description": "The topic or question to research"},
@@ -64,7 +105,7 @@ async def research_topic_tool(*, user_id, arguments, **_ctx):
@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.",
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 lookup for those.",
parameters={
"query": {"type": "string", "description": "The image search query"},
},
+61
View File
@@ -0,0 +1,61 @@
import pytest
from unittest.mock import AsyncMock, patch
@pytest.mark.asyncio
async def test_lookup_wikipedia_hit():
wiki_data = {
"title": "QUIC",
"extract": "QUIC is a transport layer protocol.",
"url": "https://en.wikipedia.org/wiki/QUIC",
}
with patch("fabledassistant.services.wikipedia.wiki_summary", new_callable=AsyncMock, return_value=wiki_data):
from fabledassistant.services.tools.web import lookup_tool
result = await lookup_tool(user_id=1, arguments={"query": "QUIC"})
assert result["success"] is True
assert result["type"] == "lookup"
assert result["source"] == "wikipedia"
assert result["data"]["title"] == "QUIC"
@pytest.mark.asyncio
async def test_lookup_wikipedia_miss_searxng_fallback():
searxng_results = [
{"url": "https://example.com/quic", "title": "QUIC Explained", "snippet": "An overview..."},
]
with patch("fabledassistant.services.wikipedia.wiki_summary", new_callable=AsyncMock, return_value=None), \
patch("fabledassistant.config.Config") as mock_config, \
patch("fabledassistant.services.research._search_searxng", new_callable=AsyncMock, return_value=searxng_results), \
patch("fabledassistant.services.rss._fetch_full_article", new_callable=AsyncMock, return_value="Full article about QUIC..."):
mock_config.searxng_enabled.return_value = True
from fabledassistant.services.tools.web import lookup_tool
result = await lookup_tool(user_id=1, arguments={"query": "QUIC"})
assert result["success"] is True
assert result["source"] == "web"
assert result["data"]["results"]
@pytest.mark.asyncio
async def test_lookup_wikipedia_miss_no_searxng():
with patch("fabledassistant.services.wikipedia.wiki_summary", new_callable=AsyncMock, return_value=None), \
patch("fabledassistant.config.Config") as mock_config:
mock_config.searxng_enabled.return_value = False
from fabledassistant.services.tools.web import lookup_tool
result = await lookup_tool(user_id=1, arguments={"query": "xyznonexistent"})
assert result["success"] is True
assert result["source"] == "none"
@pytest.mark.asyncio
async def test_lookup_always_available():
with patch("fabledassistant.services.tools._registry.is_caldav_configured", new_callable=AsyncMock, return_value=False), \
patch("fabledassistant.services.settings.get_setting", new_callable=AsyncMock, return_value="false"):
from fabledassistant.services.tools import get_tools_for_user
tools = await get_tools_for_user(user_id=1)
tool_names = {t["function"]["name"] for t in tools}
assert "lookup" in tool_names