From 73f8497232377548c4d15f5e28d408bad51b5e3a Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 2 Dec 2025 18:36:18 +0100 Subject: [PATCH] feat(core-ai): add web search tool and enhance agent persona MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add SearXNG-powered web search tool and update agent persona to "Tatlock", a helpful British butler assistant. Changes: - Add web_search tool for SearXNG metasearch integration - Supports multiple search categories (general, it, science, news, etc.) - Configurable max_results (1-20) - Privacy-focused (no tracking via SearXNG) - Formatted results with titles, URLs, and descriptions - Proper error handling for timeouts and failures - Endpoint: http://searxng:8080/search - Update agent persona to "Tatlock" (British butler) - Formal yet personable tone - Addresses users as "sir" - Fact verification emphasis - Slight snark and puns when appropriate - Clear tool categorization in prompt - Enhance prompt with tool organization - Core tools: web_search, calculate, time/date - Infrastructure tools: core_api__* prefix for system management - Clear usage guidelines for each category Web Search Categories Supported: - general: Web search (Google, Bing, DuckDuckGo) - it: Programming/technical (StackOverflow, GitHub) - science: Academic (arXiv, PubMed, Semantic Scholar) - news: News articles - images/videos: Media search - map: Geographic queries Integration: Requires SearXNG service running on docker-dataplane network. See stacks/searxng.yml for deployment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- services/core-ai/src/prompts.py | 21 +++++-- services/core-ai/src/tools/local.py | 92 ++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 5 deletions(-) diff --git a/services/core-ai/src/prompts.py b/services/core-ai/src/prompts.py index c1089a7..b671258 100644 --- a/services/core-ai/src/prompts.py +++ b/services/core-ai/src/prompts.py @@ -7,13 +7,26 @@ This file contains minimal, clean prompts for the Core AI service. PROMPTS = { "minimal_agent": """You are a helpful assistant. You can answer questions. If you need information, use the available tools.""", - "pydantic_agent": """You are a helpful assistant with access to tools. + "pydantic_agent": """You are Tatlock, a helpful personal assistant with the demeanor of a British butler. Address users as "sir" and maintain a formal yet personable tone. You are not overly apologetic and may be slightly snarky when appropriate. If an opportunity for a pun presents itself, you cannot resist. -CRITICAL: When you have tools available, you MUST use them to get accurate, real-time information. NEVER guess or make up answers when a tool can provide the actual data. +Your core responsibility: Verify facts before presenting them as truth. -For specific time queries (like "what time is it in Amsterdam?"), use the get_current_time(timezone) tool. +You have access to two categories of tools: -Be concise and thorough in your responses.""" +**Core Tools** (essential utilities): +- web_search: Latest/current/recent information (always verify facts, sir) +- calculate: Mathematical operations (precision is paramount) +- get_current_time/get_current_date: Time/date queries + +**Infrastructure Tools** (prefixed with "core_api__"): +When managing sir's home infrastructure, use these tools: +- Services: List, start, stop Docker services +- Domains: List configured domains +- Proxy: Manage reverse proxy configurations +- Monitors: Health monitoring (Uptime Kuma integration) +- Ports: Check allocated ports + +Be concise unless details are specifically requested. When using tools, acknowledge them naturally in your dignified manner.""" } diff --git a/services/core-ai/src/tools/local.py b/services/core-ai/src/tools/local.py index 6e66f03..8fcdbe5 100644 --- a/services/core-ai/src/tools/local.py +++ b/services/core-ai/src/tools/local.py @@ -2,12 +2,13 @@ Local utility tools for the AI agent. These tools run locally in core-ai and don't require REST calls. -They provide basic utilities like time, date, and calculations. +They provide basic utilities like time, date, calculations, and web search. """ import logging from datetime import datetime, timedelta from typing import Optional import pytz +import httpx from src.tools.registry import register_tool logger = logging.getLogger(__name__) @@ -167,3 +168,92 @@ async def calculate(expression: str) -> str: except Exception as e: logger.error(f"Calculation error: {e}") return f"Error: Could not evaluate expression - {type(e).__name__}" + + +@register_tool +async def web_search(query: str, category: str = "general", max_results: int = 5) -> str: + """ + Search the web using SearXNG metasearch engine. + + Aggregates results from multiple search engines (Google, Bing, DuckDuckGo, etc.) + while maintaining privacy - no tracking or data collection. + + Args: + query: Search query string (e.g., "Python programming best practices") + category: Search category - options: + "general" (default) - Web search + "images" - Image search + "videos" - Video search + "news" - News articles + "it" - Programming/technical (StackOverflow, GitHub, docs) + "science" - Academic (arXiv, PubMed, Semantic Scholar) + "map" - Geographic/location + "music" - Music/audio + "files" - File repositories + max_results: Maximum number of results to return (default: 5, max: 20) + + Returns: + Formatted search results with titles, URLs, and descriptions + + Examples: + - web_search("kubernetes deployment strategies") + - web_search("docker best practices", category="it") + - web_search("climate change research", category="science") + """ + logger.info(f"Web search: query='{query}', category='{category}', max_results={max_results}") + + try: + # Limit max_results to prevent overwhelming responses + max_results = min(max_results, 20) + + # Call SearXNG JSON API + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.get( + "http://searxng:8080/search", + params={ + "q": query, + "format": "json", + "categories": category + } + ) + response.raise_for_status() + + data = response.json() + results = data.get("results", []) + + if not results: + return f"No results found for: {query}" + + # Format results for LLM consumption + formatted_results = [] + for i, result in enumerate(results[:max_results], 1): + title = result.get("title", "No title") + url = result.get("url", "") + content = result.get("content", "No description available") + engine = result.get("engine", "unknown") + + formatted_results.append( + f"{i}. **{title}**\n" + f" URL: {url}\n" + f" {content}\n" + f" (Source: {engine})" + ) + + summary = f"Found {len(results)} total results for '{query}' (showing top {len(formatted_results)}):\n\n" + summary += "\n\n".join(formatted_results) + + logger.info(f"Web search completed: {len(formatted_results)} results returned") + return summary + + except httpx.TimeoutException: + error_msg = "Web search timed out. The search engine may be slow or unavailable." + logger.error(error_msg) + return error_msg + except httpx.HTTPStatusError as e: + error_msg = f"Web search failed with HTTP {e.response.status_code}" + logger.error(error_msg) + return error_msg + except Exception as e: + error_msg = f"Web search error: {str(e)}" + logger.error(error_msg) + return error_msg