Files
wp-agentic-writer/docs/implementation/BRAVE_SEARCH_IMPLEMENTATION_PLAN.md

41 KiB
Raw Blame History

WP Agentic Writer: Brave Search Integration Implementation Plan

Date: January 29, 2026
Status: Planning Phase
Integration Type: Seamless addition to existing plugin architecture


Executive Summary

This document outlines the complete implementation plan for integrating Brave Search API into WP Agentic Writer as an alternative web search provider alongside the existing OpenRouter :online models. The integration follows the plugin's existing architecture patterns and provides users with flexible, cost-effective web research capabilities.

Key Design Principle: Brave Search API is positioned as an alternative provider to OpenRouter's online models, giving users choice based on cost, performance, and feature requirements.


Table of Contents

  1. Architecture Overview
  2. Settings Integration
  3. Database Schema
  4. Provider Architecture
  5. REST API Endpoints
  6. Frontend Integration
  7. Agent Integration
  8. Cost Tracking Integration
  9. Implementation Phases
  10. File Structure
  11. Testing Strategy

Architecture Overview

Current Plugin Structure

wp-agentic-writer/
├── includes/
│   ├── class-openrouter-provider.php    ← Existing AI provider
│   ├── class-gutenberg-sidebar.php      ← Main REST API handler
│   ├── class-settings.php               ← Settings management
│   ├── class-cost-tracker.php           ← Cost tracking system
│   └── class-markdown-parser.php        ← Content parsing
├── assets/
│   └── js/
│       └── sidebar.js                   ← Frontend React app
└── views/
    └── settings/
        └── tab-models.php               ← Model settings UI

Integration Points

┌─────────────────────────────────────────────────────────────────┐
│ SETTINGS PANEL                                                   │
│                                                                  │
│ API Configuration                                                │
│ ├── OpenRouter API Key        [sk-or-v1-...]                   │
│ └── Brave Search API Key      [BSA...] ← NEW                   │
│                                                                  │
│ Web Search Provider                                              │
│ ├── ○ OpenRouter :online models (perplexity, etc)              │
│ └── ○ Brave Search API (independent index) ← NEW               │
│                                                                  │
│ [Only show if Brave API key is set]                            │
└─────────────────────────────────────────────────────────────────┘

Settings Integration

1. Settings Schema Extension

File: includes/class-settings.php

Add Brave Search settings to existing settings array:

// In sanitize_settings() method - line ~320
$sanitized['openrouter_api_key'] = trim( $input['openrouter_api_key'] ?? '' );

// ADD NEW:
$sanitized['brave_api_key'] = trim( $input['brave_api_key'] ?? '' );
$sanitized['brave_api_tier'] = sanitize_text_field( $input['brave_api_tier'] ?? 'base_ai' );
$sanitized['web_search_provider'] = sanitize_text_field( $input['web_search_provider'] ?? 'openrouter' );
$sanitized['brave_search_enabled'] = isset( $input['brave_search_enabled'] ) ? 1 : 0;
$sanitized['brave_cache_enabled'] = isset( $input['brave_cache_enabled'] ) ? 1 : 0;
$sanitized['brave_cache_duration_days'] = absint( $input['brave_cache_duration_days'] ?? 30 );
$sanitized['brave_monthly_budget'] = floatval( $input['brave_monthly_budget'] ?? 50.00 );
$sanitized['brave_include_citations'] = isset( $input['brave_include_citations'] ) ? 1 : 0;

2. Settings UI - API Keys Section

File: views/settings/tab-models.php

Add Brave API key field after OpenRouter API key (around line 480):

<!-- Existing OpenRouter API Key field -->
<div class="wpaw-field-row">
    <div class="wpaw-field-label">
        <?php esc_html_e( 'OpenRouter API Key', 'wp-agentic-writer' ); ?>
        <small><?php esc_html_e( 'Required for all AI features', 'wp-agentic-writer' ); ?></small>
    </div>
    <div class="wpaw-field-input">
        <input type="password" id="openrouter_api_key" name="wp_agentic_writer_settings[openrouter_api_key]" value="<?php echo esc_attr( $api_key ); ?>" />
    </div>
</div>

<!-- NEW: Brave Search API Key -->
<div class="wpaw-field-row">
    <div class="wpaw-field-label">
        <?php esc_html_e( 'Brave Search API Key', 'wp-agentic-writer' ); ?>
        <small><?php esc_html_e( 'Optional - for web research & citations', 'wp-agentic-writer' ); ?></small>
    </div>
    <div class="wpaw-field-input">
        <input type="password" id="brave_api_key" name="wp_agentic_writer_settings[brave_api_key]" value="<?php echo esc_attr( $brave_api_key ); ?>" placeholder="BSA..." />
        <p class="description">
            <?php printf( 
                wp_kses_post( __( 'Get your API key from <a href="%s" target="_blank">Brave Search API</a>. Free tier: 2,000 queries/month.', 'wp-agentic-writer' ) ), 
                'https://brave.com/search/api/' 
            ); ?>
        </p>
    </div>
</div>

<!-- NEW: Brave API Tier Selection -->
<div class="wpaw-field-row" id="brave-tier-row" style="display: <?php echo !empty($brave_api_key) ? 'flex' : 'none'; ?>;">
    <div class="wpaw-field-label">
        <?php esc_html_e( 'Brave API Tier', 'wp-agentic-writer' ); ?>
    </div>
    <div class="wpaw-field-input">
        <select name="wp_agentic_writer_settings[brave_api_tier]">
            <option value="free" <?php selected( $brave_api_tier, 'free' ); ?>>Free (2,000/month)</option>
            <option value="base_ai" <?php selected( $brave_api_tier, 'base_ai' ); ?>>Data for AI - Base ($5/1K)</option>
            <option value="pro_ai" <?php selected( $brave_api_tier, 'pro_ai' ); ?>>Data for AI - Pro ($9/1K)</option>
        </select>
    </div>
</div>

3. Settings UI - Web Search Provider Selection

Add new section after model presets:

<!-- NEW SECTION: Web Search Configuration -->
<div class="wpaw-section" id="brave-search-section" style="display: <?php echo !empty($brave_api_key) ? 'block' : 'none'; ?>;">
    <div class="wpaw-section-header">
        <h3><?php esc_html_e( 'Web Search Provider', 'wp-agentic-writer' ); ?></h3>
        <p class="description"><?php esc_html_e( 'Choose how the agent performs web research during article generation.', 'wp-agentic-writer' ); ?></p>
    </div>
    <div class="wpaw-section-body">
        
        <!-- Provider Selection -->
        <div class="wpaw-field-row">
            <div class="wpaw-field-label">
                <?php esc_html_e( 'Search Provider', 'wp-agentic-writer' ); ?>
            </div>
            <div class="wpaw-field-input">
                <label style="display: block; margin-bottom: 10px;">
                    <input type="radio" name="wp_agentic_writer_settings[web_search_provider]" value="openrouter" <?php checked( $web_search_provider, 'openrouter' ); ?> />
                    <strong>OpenRouter :online Models</strong>
                    <p class="description" style="margin-left: 24px;">
                        Uses models like Perplexity Sonar with built-in web search. Higher cost (~$5-15 per 1M tokens), but includes AI reasoning.
                    </p>
                </label>
                
                <label style="display: block;">
                    <input type="radio" name="wp_agentic_writer_settings[web_search_provider]" value="brave" <?php checked( $web_search_provider, 'brave' ); ?> />
                    <strong>Brave Search API</strong>
                    <p class="description" style="margin-left: 24px;">
                        Independent search index with 30B+ pages. Lower cost ($5-9 per 1K searches), includes caching. Agent processes results separately.
                    </p>
                </label>
            </div>
        </div>
        
        <!-- Brave-specific settings (show only when Brave is selected) -->
        <div id="brave-specific-settings" style="display: <?php echo $web_search_provider === 'brave' ? 'block' : 'none'; ?>; margin-top: 20px; padding-left: 20px; border-left: 3px solid #2271b1;">
            
            <div class="wpaw-field-row">
                <div class="wpaw-field-label">
                    <?php esc_html_e( 'Enable Search Caching', 'wp-agentic-writer' ); ?>
                </div>
                <div class="wpaw-field-input">
                    <label>
                        <input type="checkbox" name="wp_agentic_writer_settings[brave_cache_enabled]" value="1" <?php checked( $brave_cache_enabled, 1 ); ?> />
                        <?php esc_html_e( 'Cache search results to reduce API calls (recommended)', 'wp-agentic-writer' ); ?>
                    </label>
                    <p class="description">Reuses search results for identical queries. Can save 40-60% on costs.</p>
                </div>
            </div>
            
            <div class="wpaw-field-row">
                <div class="wpaw-field-label">
                    <?php esc_html_e( 'Cache Duration', 'wp-agentic-writer' ); ?>
                </div>
                <div class="wpaw-field-input">
                    <input type="number" name="wp_agentic_writer_settings[brave_cache_duration_days]" value="<?php echo esc_attr( $brave_cache_duration_days ); ?>" min="1" max="90" /> days
                    <p class="description">How long to keep cached search results (1-90 days).</p>
                </div>
            </div>
            
            <div class="wpaw-field-row">
                <div class="wpaw-field-label">
                    <?php esc_html_e( 'Monthly Budget Limit', 'wp-agentic-writer' ); ?>
                </div>
                <div class="wpaw-field-input">
                    $<input type="number" name="wp_agentic_writer_settings[brave_monthly_budget]" value="<?php echo esc_attr( $brave_monthly_budget ); ?>" min="0" step="0.01" style="width: 100px;" />
                    <p class="description">Stop searches when monthly cost exceeds this amount.</p>
                </div>
            </div>
            
            <div class="wpaw-field-row">
                <div class="wpaw-field-label">
                    <?php esc_html_e( 'Include Citations', 'wp-agentic-writer' ); ?>
                </div>
                <div class="wpaw-field-input">
                    <label>
                        <input type="checkbox" name="wp_agentic_writer_settings[brave_include_citations]" value="1" <?php checked( $brave_include_citations, 1 ); ?> />
                        <?php esc_html_e( 'Add numbered citations [1], [2]... and References section', 'wp-agentic-writer' ); ?>
                    </label>
                </div>
            </div>
            
        </div>
        
    </div>
</div>

<script>
jQuery(document).ready(function($) {
    // Show/hide Brave sections based on API key
    $('#brave_api_key').on('input', function() {
        const hasKey = $(this).val().trim().length > 0;
        $('#brave-tier-row, #brave-search-section').toggle(hasKey);
    });
    
    // Show/hide Brave-specific settings based on provider selection
    $('input[name="wp_agentic_writer_settings[web_search_provider]"]').on('change', function() {
        $('#brave-specific-settings').toggle($(this).val() === 'brave');
    });
});
</script>

Database Schema

New Tables

Create three new tables for Brave Search integration. Add to plugin activation hook.

File: wp-agentic-writer.php (activation hook)

function wp_agentic_writer_activate() {
    global $wpdb;
    $charset_collate = $wpdb->get_charset_collate();
    
    // Existing tables...
    
    // NEW: Brave Search tables
    $sql_searches = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpaw_searches (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        post_id BIGINT NOT NULL,
        search_query VARCHAR(500) NOT NULL,
        search_number INT,
        total_searches_for_post INT,
        results_count INT,
        results_json LONGTEXT,
        top_result_title VARCHAR(255),
        top_result_url VARCHAR(500),
        cost DECIMAL(10, 4),
        api_tier VARCHAR(50),
        cache_enabled TINYINT DEFAULT 1,
        cache_expires_at TIMESTAMP NULL,
        cache_hit TINYINT DEFAULT 0,
        search_category VARCHAR(100),
        status VARCHAR(30) DEFAULT 'completed',
        error_message TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        KEY idx_post (post_id),
        KEY idx_query (search_query(255)),
        KEY idx_cache_expires (cache_expires_at),
        KEY idx_status (status)
    ) $charset_collate;";
    
    $sql_citations = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpaw_citations (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        post_id BIGINT NOT NULL,
        citation_number INT NOT NULL,
        citation_text VARCHAR(500),
        context_excerpt TEXT,
        search_id BIGINT,
        source_url VARCHAR(500) NOT NULL,
        source_title VARCHAR(255),
        source_domain VARCHAR(100),
        source_type VARCHAR(50),
        result_position INT,
        article_section VARCHAR(100),
        added_by VARCHAR(50),
        verified TINYINT DEFAULT 0,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        KEY idx_post (post_id),
        KEY idx_citation_number (post_id, citation_number),
        KEY idx_source_domain (source_domain)
    ) $charset_collate;";
    
    $sql_cache = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpaw_search_cache (
        id BIGINT AUTO_INCREMENT PRIMARY KEY,
        search_query_normalized VARCHAR(500) NOT NULL,
        search_category VARCHAR(100),
        cache_key VARCHAR(64),
        results_json LONGTEXT,
        result_count INT,
        cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        expires_at TIMESTAMP,
        hit_count INT DEFAULT 0,
        cost_saved DECIMAL(10, 4) DEFAULT 0,
        quality_score DECIMAL(3,2),
        UNIQUE KEY unique_query_category (search_query_normalized(255), search_category(100)),
        KEY idx_expires (expires_at),
        KEY idx_hit_count (hit_count)
    ) $charset_collate;";
    
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql_searches );
    dbDelta( $sql_citations );
    dbDelta( $sql_cache );
}

Also add to: CREATE_TABLE.sql for manual creation


Provider Architecture

New File: includes/class-brave-search-provider.php

Create a new provider class following the same pattern as OpenRouter provider:

<?php
/**
 * Brave Search API Provider
 *
 * Handles web search via Brave Search API with caching and cost tracking.
 *
 * @package WP_Agentic_Writer
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class WP_Agentic_Writer_Brave_Search_Provider {
    
    private static $instance = null;
    private $api_key;
    private $api_base = 'https://api.search.brave.com/res/v1';
    private $tier;
    
    public static function get_instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    private function __construct() {
        $settings = get_option( 'wp_agentic_writer_settings', array() );
        $this->api_key = $settings['brave_api_key'] ?? '';
        $this->tier = $settings['brave_api_tier'] ?? 'base_ai';
    }
    
    /**
     * Perform web search
     *
     * @param string $query Search query
     * @param array  $options Search options
     * @return array|WP_Error Search results or error
     */
    public function search( $query, $options = array() ) {
        if ( empty( $this->api_key ) ) {
            return new WP_Error( 'no_api_key', 'Brave Search API key not configured' );
        }
        
        // Check cache first
        if ( ! empty( $options['use_cache'] ) ) {
            $cached = $this->get_cached_result( $query, $options['category'] ?? null );
            if ( $cached ) {
                return array(
                    'results' => $cached['results'],
                    'from_cache' => true,
                    'cache_age_hours' => $cached['age_hours'],
                    'cost' => 0,
                );
            }
        }
        
        // Check budget
        $budget_check = $this->check_budget();
        if ( is_wp_error( $budget_check ) ) {
            return $budget_check;
        }
        
        // Call API
        $response = $this->call_api( $query, $options );
        
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        
        // Calculate cost
        $cost = $this->calculate_cost();
        
        // Store search record
        $search_id = $this->store_search( $query, $response, $cost, $options );
        
        // Cache results
        if ( ! empty( $options['use_cache'] ) ) {
            $this->cache_results( $query, $response, $options );
        }
        
        // Track cost
        $this->track_cost( $cost, $options['post_id'] ?? 0 );
        
        return array(
            'results' => $response,
            'from_cache' => false,
            'search_id' => $search_id,
            'cost' => $cost,
            'result_count' => count( $response['web']['results'] ?? array() ),
        );
    }
    
    /**
     * Call Brave Search API
     */
    private function call_api( $query, $options = array() ) {
        $endpoint = $this->api_base . '/web/search';
        
        $params = array(
            'q' => $query,
            'count' => $options['count'] ?? 10,
            'safesearch' => 'moderate',
            'search_lang' => $options['language'] ?? 'en',
            'country' => $options['country'] ?? 'US',
        );
        
        $response = wp_remote_get(
            add_query_arg( $params, $endpoint ),
            array(
                'headers' => array(
                    'X-Subscription-Token' => $this->api_key,
                    'Accept' => 'application/json',
                ),
                'timeout' => 30,
            )
        );
        
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        
        $status = wp_remote_retrieve_response_code( $response );
        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        
        if ( 429 === $status ) {
            return new WP_Error( 'rate_limited', 'Brave API rate limit exceeded' );
        } elseif ( 401 === $status ) {
            return new WP_Error( 'invalid_api_key', 'Invalid Brave API key' );
        } elseif ( 200 !== $status ) {
            return new WP_Error( 'api_error', 'Brave API error: ' . $status );
        }
        
        return $body;
    }
    
    /**
     * Check cache for existing results
     */
    private function get_cached_result( $query, $category = null ) {
        global $wpdb;
        
        $cache_key = sha1( strtolower( trim( $query ) ) );
        
        $cached = $wpdb->get_row( $wpdb->prepare(
            "SELECT * FROM {$wpdb->prefix}wpaw_search_cache
             WHERE cache_key = %s
             AND expires_at > NOW()
             ORDER BY hit_count DESC
             LIMIT 1",
            $cache_key
        ) );
        
        if ( $cached ) {
            // Update cache stats
            $wpdb->update(
                $wpdb->prefix . 'wpaw_search_cache',
                array(
                    'hit_count' => $cached->hit_count + 1,
                    'cost_saved' => $cached->cost_saved + $this->calculate_cost(),
                ),
                array( 'id' => $cached->id )
            );
            
            $age_seconds = time() - strtotime( $cached->cached_at );
            
            return array(
                'results' => json_decode( $cached->results_json, true ),
                'age_hours' => ceil( $age_seconds / 3600 ),
            );
        }
        
        return null;
    }
    
    /**
     * Store search in database
     */
    private function store_search( $query, $response, $cost, $options = array() ) {
        global $wpdb;
        
        $top_result = $response['web']['results'][0] ?? null;
        
        $wpdb->insert(
            $wpdb->prefix . 'wpaw_searches',
            array(
                'post_id' => $options['post_id'] ?? 0,
                'search_query' => $query,
                'search_number' => $options['search_number'] ?? 1,
                'total_searches_for_post' => $options['total_searches'] ?? 1,
                'results_count' => count( $response['web']['results'] ?? array() ),
                'results_json' => wp_json_encode( $response ),
                'top_result_title' => $top_result['title'] ?? null,
                'top_result_url' => $top_result['url'] ?? null,
                'cost' => $cost,
                'api_tier' => $this->tier,
                'search_category' => $options['category'] ?? 'general',
                'status' => 'completed',
            )
        );
        
        return $wpdb->insert_id;
    }
    
    /**
     * Cache search results
     */
    private function cache_results( $query, $response, $options = array() ) {
        global $wpdb;
        
        $settings = get_option( 'wp_agentic_writer_settings', array() );
        $cache_days = absint( $settings['brave_cache_duration_days'] ?? 30 );
        
        $cache_key = sha1( strtolower( trim( $query ) ) );
        
        $wpdb->insert(
            $wpdb->prefix . 'wpaw_search_cache',
            array(
                'search_query_normalized' => strtolower( trim( $query ) ),
                'search_category' => $options['category'] ?? null,
                'cache_key' => $cache_key,
                'results_json' => wp_json_encode( $response ),
                'result_count' => count( $response['web']['results'] ?? array() ),
                'expires_at' => gmdate( 'Y-m-d H:i:s', strtotime( "+{$cache_days} days" ) ),
                'quality_score' => 0.9,
            )
        );
    }
    
    /**
     * Calculate cost per search
     */
    private function calculate_cost() {
        $tiers = array(
            'free' => 0,
            'base_ai' => 0.005,
            'pro_ai' => 0.009,
        );
        
        return $tiers[ $this->tier ] ?? 0;
    }
    
    /**
     * Check monthly budget
     */
    private function check_budget() {
        global $wpdb;
        
        $settings = get_option( 'wp_agentic_writer_settings', array() );
        $budget_limit = floatval( $settings['brave_monthly_budget'] ?? 50.00 );
        
        $monthly_cost = $wpdb->get_var(
            "SELECT COALESCE(SUM(cost), 0) FROM {$wpdb->prefix}wpaw_searches
             WHERE MONTH(created_at) = MONTH(NOW())
             AND YEAR(created_at) = YEAR(NOW())"
        );
        
        if ( $monthly_cost >= $budget_limit ) {
            return new WP_Error(
                'budget_exceeded',
                sprintf( 'Monthly Brave Search budget of $%.2f exceeded', $budget_limit )
            );
        }
        
        return true;
    }
    
    /**
     * Track cost in cost tracker
     */
    private function track_cost( $cost, $post_id ) {
        do_action(
            'wp_aw_after_api_request',
            $post_id,
            'brave-search',
            'web_search',
            0, // No input tokens
            0, // No output tokens
            $cost
        );
    }
}

REST API Endpoints

Add to includes/class-gutenberg-sidebar.php

Add new REST endpoints for Brave Search:

// In register_rest_routes() method, add:

// Brave Search endpoint
register_rest_route(
    'wp-agentic-writer/v1',
    '/brave-search',
    array(
        'methods' => 'POST',
        'callback' => array( $this, 'handle_brave_search' ),
        'permission_callback' => array( $this, 'check_permissions' ),
        'args' => array(
            'query' => array(
                'required' => true,
                'type' => 'string',
            ),
            'postId' => array(
                'required' => true,
                'type' => 'integer',
            ),
            'category' => array(
                'type' => 'string',
                'default' => 'general',
            ),
            'useCache' => array(
                'type' => 'boolean',
                'default' => true,
            ),
        ),
    )
);

// Get searches for post
register_rest_route(
    'wp-agentic-writer/v1',
    '/searches/(?P<post_id>\d+)',
    array(
        'methods' => 'GET',
        'callback' => array( $this, 'handle_get_searches' ),
        'permission_callback' => array( $this, 'check_permissions' ),
    )
);

Handler Methods

/**
 * Handle Brave Search request
 */
public function handle_brave_search( $request ) {
    $query = $request->get_param( 'query' );
    $post_id = $request->get_param( 'postId' );
    $category = $request->get_param( 'category' );
    $use_cache = $request->get_param( 'useCache' );
    
    $provider = WP_Agentic_Writer_Brave_Search_Provider::get_instance();
    
    $result = $provider->search(
        $query,
        array(
            'post_id' => $post_id,
            'category' => $category,
            'use_cache' => $use_cache,
        )
    );
    
    if ( is_wp_error( $result ) ) {
        return new WP_Error(
            $result->get_error_code(),
            $result->get_error_message(),
            array( 'status' => 400 )
        );
    }
    
    return rest_ensure_response( $result );
}

/**
 * Get all searches for a post
 */
public function handle_get_searches( $request ) {
    global $wpdb;
    
    $post_id = $request->get_param( 'post_id' );
    
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return new WP_Error( 'unauthorized', 'Not allowed', array( 'status' => 403 ) );
    }
    
    $searches = $wpdb->get_results( $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}wpaw_searches
         WHERE post_id = %d
         ORDER BY created_at DESC",
        $post_id
    ) );
    
    $citations = $wpdb->get_results( $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}wpaw_citations
         WHERE post_id = %d
         ORDER BY citation_number ASC",
        $post_id
    ) );
    
    return rest_ensure_response( array(
        'searches' => $searches,
        'citations' => $citations,
        'total_cost' => array_sum( array_map( function( $s ) {
            return floatval( $s->cost );
        }, $searches ) ),
    ) );
}

Frontend Integration

Update assets/js/sidebar.js

Add Brave Search support to the frontend:

// Add to state management (around line 50)
const [webSearchProvider, setWebSearchProvider] = useState('openrouter');
const [braveSearchEnabled, setBraveSearchEnabled] = useState(false);

// Load settings on mount
useEffect(() => {
    const settings = wpAgenticWriter.settings || {};
    setWebSearchProvider(settings.web_search_provider || 'openrouter');
    setBraveSearchEnabled(settings.brave_search_enabled || false);
}, []);

// Add Brave Search function
const performBraveSearch = async (query, category = 'general') => {
    try {
        const response = await fetch(wpAgenticWriter.apiUrl + '/brave-search', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': wpAgenticWriter.nonce,
            },
            body: JSON.stringify({
                query: query,
                postId: postId,
                category: category,
                useCache: true,
            }),
        });
        
        if (!response.ok) {
            throw new Error('Brave Search failed');
        }
        
        const data = await response.json();
        
        // Track cost
        if (data.cost) {
            setCost(prev => ({ ...prev, session: prev.session + data.cost }));
        }
        
        return data;
    } catch (error) {
        console.error('Brave Search error:', error);
        return null;
    }
};

// Modify article generation to use selected provider
// In sendMessage() or executePlanFromCard(), check provider:
if (webSearchProvider === 'brave' && braveSearchEnabled) {
    // Use Brave Search flow
    // Perform searches, then pass results to agent
} else {
    // Use OpenRouter :online models
    // Existing flow
}

Agent Integration

Research Planning

Create helper class for automatic search planning:

New File: includes/class-research-planner.php

<?php
/**
 * Research Planner
 *
 * Determines what searches to perform based on article topic.
 *
 * @package WP_Agentic_Writer
 */

class WP_Agentic_Writer_Research_Planner {
    
    /**
     * Plan searches for a topic
     *
     * @param string $topic Article topic
     * @param string $depth 'basic', 'medium', 'deep'
     * @return array Array of search queries with metadata
     */
    public static function plan_searches( $topic, $depth = 'medium' ) {
        $searches = array();
        
        // Core definition search (always)
        $searches[] = array(
            'query' => "$topic what is overview",
            'category' => 'definition',
            'priority' => 'critical',
        );
        
        if ( 'medium' === $depth || 'deep' === $depth ) {
            // Current state
            $searches[] = array(
                'query' => "$topic latest news 2024",
                'category' => 'news',
                'priority' => 'high',
            );
            
            // Pricing/features
            $searches[] = array(
                'query' => "$topic pricing features comparison",
                'category' => 'pricing',
                'priority' => 'high',
            );
        }
        
        if ( 'deep' === $depth ) {
            // Use cases
            $searches[] = array(
                'query' => "$topic use cases examples",
                'category' => 'examples',
                'priority' => 'medium',
            );
            
            // Technical details
            $searches[] = array(
                'query' => "$topic documentation api guide",
                'category' => 'technical',
                'priority' => 'medium',
            );
        }
        
        return $searches;
    }
}

Citation Management

New File: includes/class-citation-manager.php

<?php
/**
 * Citation Manager
 *
 * Extracts citations from article content and links to sources.
 *
 * @package WP_Agentic_Writer
 */

class WP_Agentic_Writer_Citation_Manager {
    
    /**
     * Process citations in article content
     *
     * @param int    $post_id Post ID
     * @param string $content Article content with citation markers
     * @param array  $search_results Array of search results
     * @return array Processed content with citations
     */
    public static function process_citations( $post_id, $content, $search_results ) {
        $citations = array();
        $citation_number = 1;
        
        // Find citation markers: [source_marker]
        preg_match_all( '/\[([a-z0-9_]+)\]/i', $content, $matches );
        
        foreach ( $matches[1] as $marker ) {
            // Find matching source
            $source = self::find_source_by_marker( $marker, $search_results );
            
            if ( ! $source ) {
                continue;
            }
            
            // Store citation
            self::store_citation( $post_id, $citation_number, $source );
            
            // Replace marker with number
            $content = str_replace(
                "[$marker]",
                "[$citation_number]",
                $content
            );
            
            $citations[] = array(
                'number' => $citation_number,
                'source' => $source,
            );
            
            $citation_number++;
        }
        
        // Add References section
        $references = self::generate_references_section( $citations );
        $content .= "\n\n" . $references;
        
        return array(
            'content' => $content,
            'citations' => $citations,
        );
    }
    
    /**
     * Find source in search results
     */
    private static function find_source_by_marker( $marker, $search_results ) {
        // Match marker to search result URL/title
        foreach ( $search_results as $search ) {
            if ( empty( $search['results']['web']['results'] ) ) {
                continue;
            }
            
            foreach ( $search['results']['web']['results'] as $result ) {
                $domain = parse_url( $result['url'], PHP_URL_HOST );
                
                if ( stripos( $domain, $marker ) !== false ||
                     stripos( $result['title'], $marker ) !== false ) {
                    return array(
                        'url' => $result['url'],
                        'title' => $result['title'],
                        'domain' => $domain,
                        'snippet' => $result['description'] ?? '',
                    );
                }
            }
        }
        
        return null;
    }
    
    /**
     * Store citation in database
     */
    private static function store_citation( $post_id, $citation_number, $source ) {
        global $wpdb;
        
        $wpdb->insert(
            $wpdb->prefix . 'wpaw_citations',
            array(
                'post_id' => $post_id,
                'citation_number' => $citation_number,
                'source_url' => $source['url'],
                'source_title' => $source['title'],
                'source_domain' => $source['domain'],
                'added_by' => 'agent_automatic',
            )
        );
    }
    
    /**
     * Generate References section HTML
     */
    private static function generate_references_section( $citations ) {
        if ( empty( $citations ) ) {
            return '';
        }
        
        $html = "## References\n\n";
        
        foreach ( $citations as $citation ) {
            $source = $citation['source'];
            $html .= sprintf(
                "%d. [%s](%s) - %s\n",
                $citation['number'],
                $source['title'],
                $source['url'],
                $source['domain']
            );
        }
        
        return $html;
    }
}

Cost Tracking Integration

Brave Search costs are automatically tracked via the existing cost tracking system using the wp_aw_after_api_request action hook (already implemented in the provider class).

Display in Sidebar

Update cost display to show Brave Search costs separately:

// In sidebar.js cost display section
<div className="wpaw-cost-breakdown">
    <div>AI Models: ${cost.session.toFixed(4)}</div>
    {braveSearchCost > 0 && (
        <div>Web Search: ${braveSearchCost.toFixed(4)}</div>
    )}
    <div><strong>Total: ${(cost.session + braveSearchCost).toFixed(4)}</strong></div>
</div>

Implementation Phases

Phase 1: Foundation (Week 1)

Goal: Basic Brave Search integration

  • Create database tables (activation hook)
  • Create class-brave-search-provider.php
  • Add settings fields to class-settings.php
  • Add UI to tab-models.php
  • Test API connectivity

Deliverable: Can perform Brave searches via settings panel test button

Phase 2: REST API (Week 2)

Goal: Frontend can call Brave Search

  • Add REST endpoints to class-gutenberg-sidebar.php
  • Add frontend functions to sidebar.js
  • Test search from frontend
  • Implement caching logic
  • Test cache hit/miss scenarios

Deliverable: Frontend can perform searches and see cached results

Phase 3: Agent Integration (Week 3)

Goal: Agent uses Brave Search during article generation

  • Create class-research-planner.php
  • Create class-citation-manager.php
  • Integrate into article generation flow
  • Add provider selection logic
  • Test end-to-end: topic → searches → article with citations

Deliverable: Can generate articles with Brave Search and citations

Phase 4: Analytics & Polish (Week 4)

Goal: Admin dashboard and optimization

  • Add search analytics tab to settings
  • Display cost breakdown
  • Show cache performance
  • Add budget alerts
  • Implement cache cleanup cron
  • Add error handling and logging

Deliverable: Complete admin experience with analytics

Phase 5: Testing & Documentation (Week 5)

Goal: Production-ready

  • Test with free tier limits
  • Test with paid tiers
  • Test budget enforcement
  • Test cache expiry
  • Write user documentation
  • Create migration guide

Deliverable: Production-ready feature with docs


File Structure

New Files to Create

includes/
├── class-brave-search-provider.php      ← Main provider class
├── class-research-planner.php           ← Auto search planning
└── class-citation-manager.php           ← Citation extraction

views/settings/
└── tab-brave-analytics.php              ← Analytics dashboard (optional)

Files to Modify

includes/
├── class-settings.php                   ← Add Brave settings
├── class-gutenberg-sidebar.php          ← Add REST endpoints
└── wp-agentic-writer.php                ← Add table creation

views/settings/
└── tab-models.php                       ← Add Brave UI

assets/js/
└── sidebar.js                           ← Add Brave search functions

CREATE_TABLE.sql                         ← Add table schemas

Testing Strategy

Unit Tests

  1. Provider Tests

    • API authentication
    • Search query formatting
    • Response parsing
    • Error handling
  2. Cache Tests

    • Cache hit/miss
    • Expiry logic
    • Cost savings calculation
  3. Budget Tests

    • Monthly limit enforcement
    • Alert triggering
    • Cost tracking accuracy

Integration Tests

  1. Settings Flow

    • Save Brave API key
    • Switch providers
    • Enable/disable features
  2. Search Flow

    • Perform search
    • Cache result
    • Reuse cached result
    • Track cost
  3. Article Generation Flow

    • Plan searches
    • Execute searches
    • Generate article with citations
    • Add References section

User Acceptance Tests

  1. Free Tier User

    • Set up with free API key
    • Generate article (2-3 searches)
    • Verify cost tracking
    • Test monthly limit
  2. Paid Tier User

    • Set up with paid API key
    • Generate multiple articles
    • Verify cache reuse
    • Check cost savings
  3. OpenRouter User

    • Keep using OpenRouter :online
    • Verify no breaking changes
    • Test switching between providers

Migration & Compatibility

Backward Compatibility

  • No breaking changes: Existing OpenRouter functionality remains unchanged
  • Opt-in feature: Brave Search only activates when API key is set
  • Default behavior: If no Brave API key, falls back to OpenRouter :online
  • Settings migration: No migration needed - new settings are additive

Rollout Strategy

  1. Beta Phase: Release to select users for testing
  2. Documentation: Create setup guide and comparison chart
  3. Announcement: Blog post explaining benefits
  4. Support: Monitor for issues and provide quick fixes

Cost Comparison Example

Scenario: Generate 10 articles with web research

OpenRouter :online (Perplexity Sonar Pro)

  • Model: perplexity/sonar-pro
  • Cost: ~$15 per 1M tokens
  • Average: 50K tokens per article with research
  • Total: 500K tokens = $7.50

Brave Search API + Standard Model

  • Brave: 3 searches per article × 10 articles = 30 searches
  • Brave cost: 30 × $0.005 = $0.15
  • AI model: google/gemini-2.0-flash-exp (free or $0.075/1M)
  • AI tokens: 30K tokens per article (no search overhead)
  • AI cost: 300K tokens = $0.02
  • Total: $0.17 (97% cheaper!)

With Caching (60% hit rate)

  • Brave: 12 fresh + 18 cached = 12 × $0.005 = $0.06
  • AI cost: $0.02
  • Total: $0.08 (99% cheaper!)

Next Steps

  1. Review this plan with stakeholders
  2. Set up Brave Search API account (free tier for testing)
  3. Begin Phase 1 implementation (database + provider class)
  4. Create test environment with sample searches
  5. Iterate based on feedback

Questions & Decisions Needed

  1. Should we support both providers simultaneously?

    • Current plan: User chooses one provider per article
    • Alternative: Use both (Brave for facts, OpenRouter for reasoning)
  2. Citation format preference?

    • Current plan: Numbered [1], [2]... with References section
    • Alternative: Inline links, footnotes, or custom format
  3. Cache invalidation strategy?

    • Current plan: 30-day automatic expiry
    • Alternative: Manual invalidation, topic-based expiry
  4. Budget alert method?

    • Current plan: Email to admin
    • Alternative: Dashboard notification, Slack webhook

Document Status: Ready for Implementation
Last Updated: January 29, 2026
Version: 1.0