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:
@@ -1,4 +1,4 @@
|
|||||||
"""Web search, research, and image tools (require SearXNG)."""
|
"""Web search, research, and image tools."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -10,29 +10,70 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@tool(
|
@tool(
|
||||||
name="search_web",
|
name="lookup",
|
||||||
description=(
|
description=(
|
||||||
"Quick web lookup — returns search result snippets for factual questions, current events, "
|
"Look up a topic, concept, or factual question. Returns a concise answer from "
|
||||||
"definitions, or version numbers. No note is saved. Use research_topic when the user wants "
|
"Wikipedia or web sources. Use for definitions, explanations, 'what is X', "
|
||||||
"a comprehensive written report saved as a note."
|
"'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={
|
parameters={
|
||||||
"query": {"type": "string", "description": "The search query"},
|
"query": {"type": "string", "description": "The topic or question to look up"},
|
||||||
},
|
},
|
||||||
required=["query"],
|
required=["query"],
|
||||||
requires="searxng",
|
|
||||||
)
|
)
|
||||||
async def search_web_tool(*, user_id, arguments, **_ctx):
|
async def lookup_tool(*, user_id, arguments, **_ctx):
|
||||||
from fabledassistant.services.research import _search_searxng
|
from fabledassistant.config import Config
|
||||||
|
from fabledassistant.services.wikipedia import wiki_summary
|
||||||
|
|
||||||
query = arguments.get("query", "")
|
query = arguments.get("query", "")
|
||||||
results = await _search_searxng(query)
|
|
||||||
if not results:
|
# 1. Try Wikipedia first
|
||||||
return {"success": False, "error": f"No results found for '{query}'"}
|
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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"type": "web_search",
|
"type": "lookup",
|
||||||
"data": {"query": query, "results": results, "count": len(results)},
|
"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 "
|
"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', "
|
"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. "
|
"'look into', or wants a comprehensive write-up. Takes 30–120 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={
|
parameters={
|
||||||
"topic": {"type": "string", "description": "The topic or question to research"},
|
"topic": {"type": "string", "description": "The topic or question to research"},
|
||||||
@@ -64,7 +105,7 @@ async def research_topic_tool(*, user_id, arguments, **_ctx):
|
|||||||
|
|
||||||
@tool(
|
@tool(
|
||||||
name="search_images",
|
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={
|
parameters={
|
||||||
"query": {"type": "string", "description": "The image search query"},
|
"query": {"type": "string", "description": "The image search query"},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user