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:
@@ -28,6 +28,7 @@ from fabledassistant.services.intent import IntentResult, classify_intent
|
||||
from fabledassistant.services.logging import log_generation
|
||||
from fabledassistant.services.settings import get_setting
|
||||
from fabledassistant.services.tools import get_tools_for_user, execute_tool
|
||||
from fabledassistant.services.research import run_research_pipeline
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -59,6 +60,8 @@ _TOOL_LABELS: dict[str, str] = {
|
||||
"update_todo": "Updating todo",
|
||||
"complete_todo": "Completing todo",
|
||||
"delete_todo": "Removing todo",
|
||||
"search_web": "Searching the web",
|
||||
"research_topic": "Researching topic",
|
||||
}
|
||||
|
||||
# Tools that write data and require explicit user confirmation before executing.
|
||||
@@ -285,6 +288,43 @@ async def run_generation(
|
||||
if timing["ttft_ms"] is None:
|
||||
timing["ttft_ms"] = int((time.monotonic() - t_start) * 1000)
|
||||
|
||||
if tool_name == "research_topic":
|
||||
topic = intent.arguments.get("topic", "")
|
||||
if not ack_text:
|
||||
fallback_ack = f"I'll research '{topic}' and compile a note.\n\n"
|
||||
buf.append_event("chunk", {"chunk": fallback_ack})
|
||||
buf.content_so_far += fallback_ack
|
||||
if timing["ttft_ms"] is None:
|
||||
timing["ttft_ms"] = int((time.monotonic() - t_start) * 1000)
|
||||
try:
|
||||
note = await run_research_pipeline(
|
||||
topic, user_id, model, intent_model, buf
|
||||
)
|
||||
done_text = (
|
||||
f"Research complete! I've compiled a note: "
|
||||
f"**[{note.title}](/notes/{note.id})**."
|
||||
)
|
||||
buf.append_event("chunk", {"chunk": done_text})
|
||||
buf.content_so_far += done_text
|
||||
tool_record = {
|
||||
"function": "research_topic",
|
||||
"arguments": {"topic": topic},
|
||||
"result": {
|
||||
"success": True,
|
||||
"type": "research_note",
|
||||
"data": {"id": note.id, "title": note.title},
|
||||
},
|
||||
"status": "success",
|
||||
}
|
||||
all_tool_calls.append(tool_record)
|
||||
buf.append_event("tool_call", {"tool_call": tool_record})
|
||||
except Exception as e:
|
||||
logger.exception("Research pipeline failed for topic: %s", topic)
|
||||
err_text = f"\nResearch failed: {e}"
|
||||
buf.append_event("chunk", {"chunk": err_text})
|
||||
buf.content_so_far += err_text
|
||||
break # research IS the full response
|
||||
|
||||
confirmed = True
|
||||
if tool_name in _WRITE_TOOLS:
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
Reference in New Issue
Block a user