- Implement local backend AI provider with Ollama integration - Add Brave Search API integration for real-time search suggestions - Add image generation manager with multiple AI providers - Create hybrid provider system with local/cloud fallback - Add comprehensive settings UI with provider management - Implement Gutenberg sidebar with writing assistance controls - Add SEO schema generation for AI-generated content - Multiple provider support: OpenRouter, local backend, Codex
1294 lines
41 KiB
Markdown
1294 lines
41 KiB
Markdown
# 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](#architecture-overview)
|
||
2. [Settings Integration](#settings-integration)
|
||
3. [Database Schema](#database-schema)
|
||
4. [Provider Architecture](#provider-architecture)
|
||
5. [REST API Endpoints](#rest-api-endpoints)
|
||
6. [Frontend Integration](#frontend-integration)
|
||
7. [Agent Integration](#agent-integration)
|
||
8. [Cost Tracking Integration](#cost-tracking-integration)
|
||
9. [Implementation Phases](#implementation-phases)
|
||
10. [File Structure](#file-structure)
|
||
11. [Testing Strategy](#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:
|
||
|
||
```php
|
||
// 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):
|
||
|
||
```php
|
||
<!-- 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:
|
||
|
||
```php
|
||
<!-- 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)
|
||
|
||
```php
|
||
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
|
||
<?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:
|
||
|
||
```php
|
||
// 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
|
||
|
||
```php
|
||
/**
|
||
* 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:
|
||
|
||
```javascript
|
||
// 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
|
||
<?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
|
||
<?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:
|
||
|
||
```javascript
|
||
// 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
|