Skip to content

Web Search for Agents #249

Description

@Entropy-rgb

Problem Statement

AI agents can only query the connected database. No way to search the web for context, documentation, or external data sources. Users cannot get answers that require external information.

Proposed Solution

Add web search capability to the AI agent, allowing it to search for documentation, error explanations, and external context when answering questions.

Acceptance Criteria

  • Web search tool available to AI agent
  • Configurable search provider (Google, Bing, DuckDuckGo)
  • Search triggered automatically for certain queries (error explanations, documentation lookup)
  • Search results included in context
  • Source citations in responses
  • Search rate limiting and caching
  • Privacy mode (no search logging)
  • Search preferences per workspace

Technical Approach

Backend Changes

1. Web search service (backend/app/services/web_search.py):

import httpx
from typing import Optional

class WebSearchService:
    def __init__(self, provider: str = "duckduckgo", api_key: Optional[str] = None):
        self.provider = provider
        self.api_key = api_key
        self.cache = {}
    
    async def search(self, query: str, num_results: int = 5) -> list[dict]:
        """Search the web and return results."""
        # Check cache
        cache_key = f"{query}:{num_results}"
        if cache_key in self.cache:
            return self.cache[cache_key]
        
        # Execute search based on provider
        if self.provider == "google":
            results = await self._google_search(query, num_results)
        elif self.provider == "bing":
            results = await self._bing_search(query, num_results)
        else:
            results = await self._duckduckgo_search(query, num_results)
        
        # Cache results
        self.cache[cache_key] = results
        return results
    
    async def _duckduckgo_search(self, query: str, num_results: int) -> list[dict]:
        """Search using DuckDuckGo."""
        async with httpx.AsyncClient() as client:
            response = await client.get(
                "https://api.duckduckgo.com/",
                params={"q": query, "format": "json", "no_redirect": "1"}
            )
            data = response.json()
        
        results = []
        for result in data.get("RelatedTopics", [])[:num_results]:
            if "Text" in result:
                results.append({
                    "title": result.get("Text", "")[:100],
                    "snippet": result.get("Text", ""),
                    "url": result.get("FirstURL", ""),
                    "source": "DuckDuckGo"
                })
        
        return results
    
    async def _google_search(self, query: str, num_results: int) -> list[dict]:
        """Search using Google Custom Search API."""
        async with httpx.AsyncClient() as client:
            response = await client.get(
                "https://www.googleapis.com/customsearch/v1",
                params={
                    "key": self.api_key,
                    "cx": "your_search_engine_id",
                    "q": query,
                    "num": num_results
                }
            )
            data = response.json()
        
        results = []
        for item in data.get("items", [])[:num_results]:
            results.append({
                "title": item.get("title"),
                "snippet": item.get("snippet"),
                "url": item.get("link"),
                "source": "Google"
            })
        
        return results

2. Search trigger logic (backend/app/services/search_trigger.py):

class SearchTrigger:
    def __init__(self, web_search: WebSearchService):
        self.web_search = web_search
    
    def should_search(self, question: str, context: dict) -> bool:
        """Determine if web search would help answer the question."""
        question_lower = question.lower()
        
        # Trigger for error explanations
        if any(word in question_lower for word in ["error", "exception", "why", "explain"]):
            return True
        
        # Trigger for documentation lookup
        if any(word in question_lower for word in ["how to", "documentation", "docs"]):
            return True
        
        # Trigger for external data
        if any(word in question_lower for word in ["weather", "news", "stock", "price"]):
            return True
        
        return False
    
    async def enhance_context(self, question: str, context: dict) -> dict:
        """Add web search results to context."""
        if not self.should_search(question, context):
            return context
        
        search_results = await self.web_search.search(question, num_results=3)
        
        context["web_search_results"] = search_results
        return context

3. Search integration in prompt (backend/app/llm.py):

def build_sql_system_prompt(schema_text, dialect, context=None):
    base_prompt = "...existing prompt..."
    
    # Add web search results if available
    if context and context.get("web_search_results"):
        search_context = "\n\nWeb Search Results (for context only, do not use in SQL):\n"
        for i, result in enumerate(context["web_search_results"], 1):
            search_context += f"{i}. {result['title']}: {result['snippet']}\n"
            search_context += f"   Source: {result['url']}\n"
        
        base_prompt += search_context
    
    return base_prompt

4. API routes (backend/app/routes/search.py):

  • POST /api/search - Execute web search
  • GET /api/search/preferences - Get search preferences
  • PATCH /api/search/preferences - Update preferences

Frontend Changes

1. Search preferences (frontend/src/routes/settings/search/+page.svelte):

<script>
    export let preferences;
    
    let provider = preferences.provider || 'duckduckgo';
    let apiKey = preferences.api_key || '';
    let privacyMode = preferences.privacy_mode || false;
    
    async function savePreferences() {
        await apiCall('/api/search/preferences', {
            method: 'PATCH',
            body: JSON.stringify({
                provider,
                api_key: apiKey,
                privacy_mode: privacyMode
            })
        });
    }
</script>

<div class="search-settings">
    <h1>Web Search Settings</h1>
    
    <section>
        <h2>Search Provider</h2>
        <select bind:value={provider}>
            <option value="duckduckgo">DuckDuckGo (Free)</option>
            <option value="google">Google Custom Search</option>
            <option value="bing">Bing Search</option>
        </select>
        
        {#if provider !== 'duckduckgo'}
            <label>
                API Key
                <input bind:value={apiKey} type="password" />
            </label>
        {/if}
    </section>
    
    <section>
        <h2>Privacy</h2>
        <label>
            <input type="checkbox" bind:checked={privacyMode} />
            Enable Privacy Mode (no search logging)
        </label>
    </section>
    
    <button on:click={savePreferences}>Save Preferences</button>
</div>

2. Search results display - Show web search context in chat

3. Source citations - Link to web sources in responses

Key Files

  • backend/app/services/web_search.py - Search service (new)
  • backend/app/services/search_trigger.py - Trigger logic (new)
  • backend/app/llm.py - Prompt integration
  • backend/app/routes/search.py - API routes (new)
  • frontend/src/routes/settings/search/+page.svelte - Settings

Related Issues

Metadata

Metadata

Assignees

Labels

P-5Future considerationfeature-requestNew feature request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions