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.
378 lines
11 KiB
PHP
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;
|
|
}
|
|
}
|