Files
wp-agentic-writer/includes/class-custom-search-api.php
Dwindi Ramadhana 690991c526 refactor: Cleanup git state - commit all staged changes
Major refactoring cleanup:
- Add new controller architecture (class-controller-*.php)
- Add new settings-v2 UI (views/settings-v2/)
- Add new CSS architecture (agentic-sidebar.css, tokens)
- Add esbuild build pipeline (scripts/build.js, package.json)
- Add composer dependencies (vendor/)
- Add frontend src directory (assets/js/src/index.jsx)
- Add documentation files
- Remove old/obsolete files (class-settings.php, old CSS)

This commits all pending changes from previous refactoring efforts.
2026-06-17 05:27:58 +07:00

378 lines
11 KiB
PHP

<?php
/**
* Custom Search API Integration
*
* Handles fetching web search results using generic adapters (9Router, Brave, Tavily, Serper)
*
* @package WP_Agentic_Writer
*/
if (!defined("ABSPATH")) {
exit();
}
class WP_Agentic_Writer_Custom_Search_API
{
/**
* Get singleton instance.
*
* @since 0.1.0
* @return WP_Agentic_Writer_Custom_Search_API
*/
public static function get_instance()
{
static $instance = null;
if (null === $instance) {
$instance = new self();
}
return $instance;
}
/**
* Perform a web search based on configured search engine.
*
* @since 0.1.0
* @param string $query Required. The user's search query.
* @param int $count Optional. Number of results to return. Default 3.
* @return array|WP_Error Array of formatted search results, or WP_Error on failure.
*/
public function search($query, $count = 3)
{
$settings = get_option("wp_agentic_writer_settings", []);
$api_key = $settings["brave_search_api_key"] ?? "";
$engine = $settings["search_engine"] ?? "9router";
$base_url = $settings["custom_search_url"] ?? "";
// Auto fallback to 9Router if not openrouter/auto
if ($engine === "auto" || $engine === "openrouter") {
$engine = "9router";
}
if (empty($api_key)) {
return new WP_Error(
"search_api_key_missing",
__(
"Search API Key is missing. Please configure it in WP Agentic Writer settings under Tools.",
"wp-agentic-writer",
),
);
}
// Check cache first to prevent burning API limits
$cache_key =
"wpaw_custom_search_" . md5($engine . "_" . $query . "_" . $count);
$cached_results = get_transient($cache_key);
if (false !== $cached_results) {
return $cached_results;
}
$results = [];
// -------------------------------------------------------------
// ADAPTER ROUTING
// -------------------------------------------------------------
switch ($engine) {
case "brave":
$results = $this->search_brave(
$query,
$count,
$api_key,
$base_url,
);
break;
case "tavily":
$results = $this->search_tavily(
$query,
$count,
$api_key,
$base_url,
);
break;
case "serper":
$results = $this->search_serper(
$query,
$count,
$api_key,
$base_url,
);
break;
case "9router":
default:
$results = $this->search_9router(
$query,
$count,
$api_key,
$base_url,
);
break;
}
if (is_wp_error($results)) {
return $results;
}
if (empty($results)) {
return []; // No results found
}
// Cache results for 1 hour to prevent redundant API calls
set_transient($cache_key, $results, HOUR_IN_SECONDS);
return $results;
}
/**
* Driver: 9Router Proxy (Generic OpenAI-style search endpoint)
*/
private function search_9router($query, $count, $api_key, $base_url)
{
$url = !empty($base_url)
? $base_url
: "http://localhost:20128/v1/search";
$body = [
"model" => "gemini", // generic
"query" => $query,
"search_type" => "web",
"max_results" => absint($count),
];
$response = wp_remote_post($url, [
"headers" => [
"Content-Type" => "application/json",
"Authorization" => "Bearer " . $api_key,
],
"body" => wp_json_encode($body),
"timeout" => 15,
]);
if (is_wp_error($response)) {
return $response;
}
$http_code = wp_remote_retrieve_response_code($response);
$body_response = json_decode(wp_remote_retrieve_body($response), true);
if (200 !== $http_code) {
return new WP_Error(
"search_error",
"9Router Search Error: " .
($body_response["error"]["message"] ?? "Unknown"),
);
}
$formatted = [];
if (
!empty($body_response["data"]) &&
is_array($body_response["data"])
) {
foreach ($body_response["data"] as $result) {
$formatted[] = [
"title" => $result["title"] ?? "",
"url" => $result["url"] ?? "",
"description" =>
$result["content"] ?? ($result["description"] ?? ""),
];
}
}
return $formatted;
}
/**
* Driver: Tavily API
*/
private function search_tavily($query, $count, $api_key, $base_url)
{
$url = !empty($base_url) ? $base_url : "https://api.tavily.com/search";
$body = [
"api_key" => $api_key,
"query" => $query,
"search_depth" => "basic",
"max_results" => absint($count),
];
$response = wp_remote_post($url, [
"headers" => ["Content-Type" => "application/json"],
"body" => wp_json_encode($body),
"timeout" => 15,
]);
if (is_wp_error($response)) {
return $response;
}
$http_code = wp_remote_retrieve_response_code($response);
$body_response = json_decode(wp_remote_retrieve_body($response), true);
if (200 !== $http_code) {
return new WP_Error(
"search_error",
"Tavily Search Error: " .
($body_response["detail"] ?? "Unknown"),
);
}
$formatted = [];
if (
!empty($body_response["results"]) &&
is_array($body_response["results"])
) {
foreach ($body_response["results"] as $result) {
$formatted[] = [
"title" => $result["title"] ?? "",
"url" => $result["url"] ?? "",
"description" => $result["content"] ?? "",
];
}
}
return $formatted;
}
/**
* Driver: Serper.dev
*/
private function search_serper($query, $count, $api_key, $base_url)
{
$url = !empty($base_url)
? $base_url
: "https://google.serper.dev/search";
$body = [
"q" => $query,
"num" => absint($count),
];
$response = wp_remote_post($url, [
"headers" => [
"Content-Type" => "application/json",
"X-API-KEY" => $api_key,
],
"body" => wp_json_encode($body),
"timeout" => 15,
]);
if (is_wp_error($response)) {
return $response;
}
$http_code = wp_remote_retrieve_response_code($response);
$body_response = json_decode(wp_remote_retrieve_body($response), true);
if (200 !== $http_code) {
return new WP_Error(
"search_error",
"Serper Search Error: " .
($body_response["message"] ?? "Unknown"),
);
}
$formatted = [];
if (
!empty($body_response["organic"]) &&
is_array($body_response["organic"])
) {
foreach ($body_response["organic"] as $result) {
$formatted[] = [
"title" => $result["title"] ?? "",
"url" => $result["link"] ?? "",
"description" => $result["snippet"] ?? "",
];
}
}
return $formatted;
}
/**
* Driver: Brave Search API
*/
private function search_brave($query, $count, $api_key, $base_url)
{
$base = !empty($base_url)
? $base_url
: "https://api.search.brave.com/res/v1/web/search";
$url = add_query_arg(
[
"q" => urlencode($query),
"count" => absint($count),
"text_decorations" => 0,
"spellcheck" => 1,
],
$base,
);
$response = wp_remote_get($url, [
"headers" => [
"Accept" => "application/json",
"Accept-Encoding" => "gzip",
"X-Subscription-Token" => $api_key,
],
"timeout" => 15,
]);
if (is_wp_error($response)) {
return $response;
}
$http_code = wp_remote_retrieve_response_code($response);
$body_response = json_decode(wp_remote_retrieve_body($response), true);
if (200 !== $http_code) {
return new WP_Error(
"search_error",
"Brave Search Error: " .
($body_response["message"] ?? "Unknown"),
);
}
$formatted = [];
if (
!empty($body_response["web"]["results"]) &&
is_array($body_response["web"]["results"])
) {
foreach ($body_response["web"]["results"] as $result) {
$formatted[] = [
"title" => $result["title"] ?? "",
"url" => $result["url"] ?? "",
"description" => $result["description"] ?? "",
];
}
}
return $formatted;
}
/**
* Formats search results into a markdown context block for LLM System Prompt injection.
*
* @since 0.1.0
* @param array $results Search results array.
* @param string $query Original query.
* @return string Formatted markdown context string.
*/
public function format_results_for_llm($results, $query)
{
if (empty($results) || is_wp_error($results)) {
return "No reliable web search results found for: {$query}";
}
$markdown = "## LIVE WEB SEARCH CONTEXT\n";
$markdown .= "> You successfully searched the internet for: \"{$query}\"\n";
$markdown .=
"> Please incorporate the following real-time data into your answer:\n\n";
$counter = 1;
foreach ($results as $item) {
$markdown .= "{$counter}. **{$item["title"]}**\n";
$markdown .= " URL: {$item["url"]}\n";
$markdown .= " Summary: {$item["description"]}\n\n";
$counter++;
}
$markdown .= "---------------------------\n";
return $markdown;
}
}