Phase 22: SearXNG web research pipeline + settings layout overhaul
Research pipeline (research_topic tool): - New service: services/research.py — sub-query generation, SearXNG search, URL fetch, deduplication, and LLM synthesis into a note - 5 sub-queries × 3 pages = up to 15 sources, capped at 12 for synthesis - Synthesis uses num_ctx=16384 + max_tokens=8192 for long-form output - Prompt demands 2500+ words, 6+ topic-appropriate sections, detailed prose - 429 retry with backoff; 1s inter-query sleep; raw_decode JSON parsing search_web tool (new): - Lightweight single-query SearXNG search, results returned inline in chat - LLM answers conversationally in round 1; no note created - web_search result type with external links in ToolCallCard Infrastructure: - llm.py: generate_completion accepts num_ctx override - config.py: SEARXNG_URL + Config.searxng_enabled() - docker-compose: OLLAMA_NUM_PARALLEL=2, commented SEARXNG_URL example - intent.py: search_web and research_topic routing rules Settings UI: - 2-column grid layout (small sections pair up, complex span full width) - Search Test section: live SearXNG query with result preview - GET /api/settings/search?q= proxy endpoint - Research button (magnifier) in ChatView input toolbar → popover modal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ from fabledassistant.services.caldav import (
|
||||
update_event,
|
||||
update_todo,
|
||||
)
|
||||
from fabledassistant.config import Config
|
||||
from fabledassistant.services.notes import create_note, delete_note, get_note_by_title, list_notes, update_note
|
||||
from fabledassistant.services.tag_suggestions import suggest_tags
|
||||
|
||||
@@ -663,11 +664,61 @@ _CALDAV_TOOLS = [
|
||||
]
|
||||
|
||||
|
||||
_SEARCH_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "The search query"}
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
_RESEARCH_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"topic": {
|
||||
"type": "string",
|
||||
"description": "The topic or question to research",
|
||||
}
|
||||
},
|
||||
"required": ["topic"],
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
async def get_tools_for_user(user_id: int) -> list[dict]:
|
||||
"""Build the tool list for a user based on their configured integrations."""
|
||||
tools = list(_CORE_TOOLS)
|
||||
if await is_caldav_configured(user_id):
|
||||
tools.extend(_CALDAV_TOOLS)
|
||||
if Config.searxng_enabled():
|
||||
tools.extend(_SEARCH_TOOLS)
|
||||
tools.extend(_RESEARCH_TOOLS)
|
||||
logger.debug("User %d: %d tools available", user_id, len(tools))
|
||||
return tools
|
||||
|
||||
@@ -1128,6 +1179,32 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
|
||||
"data": {"id": note.id, "title": note.title},
|
||||
}
|
||||
|
||||
elif tool_name == "search_web":
|
||||
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),
|
||||
},
|
||||
}
|
||||
|
||||
elif tool_name == "research_topic":
|
||||
# Research is always handled upstream in generation_task.py (round 0).
|
||||
# This fallback exists in case it somehow reaches execute_tool.
|
||||
topic = arguments.get("topic", "")
|
||||
return {
|
||||
"success": True,
|
||||
"type": "research_pending",
|
||||
"data": {"topic": topic},
|
||||
}
|
||||
|
||||
else:
|
||||
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user