feat(core-ai): add web search tool and enhance agent persona

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 <noreply@anthropic.com>
This commit is contained in:
2025-12-02 18:36:18 +01:00
co-authored by Claude
parent 7b128a8e6f
commit 73f8497232
2 changed files with 108 additions and 5 deletions
+17 -4
View File
@@ -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."""
}
+91 -1
View File
@@ -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