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.
800 lines
25 KiB
PHP
800 lines
25 KiB
PHP
<?php
|
|
/**
|
|
* Chat REST Controller
|
|
*
|
|
* Handles chat operations including messaging, streaming, search, and research.
|
|
*
|
|
* @package WP_Agentic_Writer
|
|
*/
|
|
|
|
/**
|
|
* Class WP_Agentic_Writer_Controller_Chat
|
|
*
|
|
* REST controller for chat operations.
|
|
*
|
|
* @since 0.3.0
|
|
*/
|
|
class WP_Agentic_Writer_Controller_Chat
|
|
{
|
|
/**
|
|
* Sidebar instance for dependency access.
|
|
*
|
|
* @var WP_Agentic_Writer_Gutenberg_Sidebar
|
|
*/
|
|
private $sidebar;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @since 0.3.0
|
|
* @param WP_Agentic_Writer_Gutenberg_Sidebar $sidebar Sidebar instance.
|
|
*/
|
|
public function __construct($sidebar)
|
|
{
|
|
$this->sidebar = $sidebar;
|
|
}
|
|
|
|
/**
|
|
* Handle chat request.
|
|
*
|
|
* @since 0.1.0
|
|
* @param WP_REST_Request $request REST request.
|
|
* @return WP_REST_Response|WP_Error Response.
|
|
*/
|
|
public function handle_chat_request($request)
|
|
{
|
|
// Check rate limit.
|
|
$params = $request->get_json_params();
|
|
$stream = !empty($params["stream"]);
|
|
$endpoint = $stream ? "chat_stream" : "chat";
|
|
|
|
$rate_limit = WPAW_Rate_Limiter::check($endpoint);
|
|
if (is_wp_error($rate_limit)) {
|
|
return $rate_limit;
|
|
}
|
|
|
|
$messages = $params["messages"] ?? [];
|
|
$post_id = $params["postId"] ?? 0;
|
|
$type = $params["type"] ?? "planning";
|
|
$session_id = $this->sidebar->resolve_or_create_session_id(
|
|
$params["sessionId"] ?? "",
|
|
$post_id,
|
|
);
|
|
|
|
// Check post permission if post_id is provided.
|
|
if ($post_id > 0 && !$this->sidebar->check_post_permission($post_id)) {
|
|
return new WP_Error(
|
|
"forbidden",
|
|
__(
|
|
"You do not have permission to access this post.",
|
|
"wp-agentic-writer",
|
|
),
|
|
["status" => 403],
|
|
);
|
|
}
|
|
|
|
$post_config = $this->sidebar->resolve_post_config_from_request(
|
|
$params,
|
|
$post_id,
|
|
);
|
|
$post_config_context = $this->sidebar->build_post_config_context(
|
|
$post_config,
|
|
);
|
|
|
|
// Detect language from user's last message for real-time response matching.
|
|
$last_user_message = $this->sidebar->get_last_user_message($messages);
|
|
$detected_from_message = $this->sidebar->detect_language_from_text(
|
|
$last_user_message,
|
|
);
|
|
$stored_language = get_post_meta(
|
|
$post_id,
|
|
"_wpaw_detected_language",
|
|
true,
|
|
);
|
|
$effective_language = $this->sidebar->resolve_language_preference(
|
|
$post_config,
|
|
$detected_from_message ?: $stored_language,
|
|
);
|
|
|
|
// Extract focus keyword for context anchoring.
|
|
$focus_keyword = "";
|
|
if (!empty($post_config["focus_keyword"])) {
|
|
$focus_keyword = sanitize_text_field($post_config["focus_keyword"]);
|
|
} elseif (!empty($post_config["seo_focus_keyword"])) {
|
|
$focus_keyword = sanitize_text_field(
|
|
$post_config["seo_focus_keyword"],
|
|
);
|
|
} elseif ($post_id > 0) {
|
|
$focus_keyword = get_post_meta(
|
|
$post_id,
|
|
"_wpaw_focus_keyword",
|
|
true,
|
|
);
|
|
}
|
|
|
|
// Build focus keyword instruction for chat.
|
|
$focus_keyword_instruction = "";
|
|
if (!empty($focus_keyword)) {
|
|
$focus_keyword_instruction = "
|
|
CONTEXT ANCHOR: The user is working on an article about \"{$focus_keyword}\".
|
|
Keep your responses relevant to this primary topic. If the conversation drifts, gently guide it back to \"{$focus_keyword}\".
|
|
|
|
At the END of your response, if you identify a good focus keyword from the discussion, suggest it in this format:
|
|
**Focus Keyword Suggestion:** [your suggested keyword]
|
|
";
|
|
}
|
|
|
|
$language_instruction = $this->sidebar->build_language_instruction(
|
|
$effective_language,
|
|
"chat responses",
|
|
);
|
|
$system_prompt = "You are a helpful writing assistant. Answer clearly, with concise structure and practical suggestions.
|
|
{$focus_keyword_instruction}
|
|
CRITICAL LANGUAGE REQUIREMENT:
|
|
{$language_instruction}
|
|
{$post_config_context}";
|
|
|
|
$context_builder = WP_Agentic_Writer_Context_Builder::get_instance();
|
|
$context_package = $context_builder->build_system_message(
|
|
"chat",
|
|
$session_id,
|
|
$post_id,
|
|
array_merge($params, [
|
|
"messages" => $messages,
|
|
"postConfig" => $post_config,
|
|
"latestUserMessage" => $last_user_message,
|
|
]),
|
|
);
|
|
|
|
// OpenRouter is stateless; send only compact saved context plus the latest turn.
|
|
$messages = [];
|
|
if ("" !== trim((string) $last_user_message)) {
|
|
$messages[] = [
|
|
"role" => "user",
|
|
"content" => $last_user_message,
|
|
];
|
|
}
|
|
|
|
$messages = $this->sidebar->prepend_system_prompt(
|
|
$messages,
|
|
$system_prompt,
|
|
);
|
|
if (!empty($context_package["message"])) {
|
|
array_splice($messages, 1, 0, [$context_package["message"]]);
|
|
}
|
|
|
|
// Get provider for this task type with selection metadata.
|
|
$provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task(
|
|
$type,
|
|
);
|
|
$provider = $provider_result->provider;
|
|
$provider_warnings = $provider_result->warnings;
|
|
|
|
if ($stream) {
|
|
$web_search_options = $this->sidebar->get_web_search_options(
|
|
$post_config,
|
|
);
|
|
$this->stream_chat_request(
|
|
$messages,
|
|
$post_id,
|
|
$type,
|
|
$web_search_options,
|
|
$session_id,
|
|
);
|
|
exit();
|
|
}
|
|
|
|
// Send chat request.
|
|
$response = $provider->chat($messages, [], $type);
|
|
|
|
if (is_wp_error($response)) {
|
|
return new WP_Error("chat_error", $response->get_error_message(), [
|
|
"status" => 500,
|
|
]);
|
|
}
|
|
|
|
// MEMANTO: Remember user message (fire-and-forget, never blocks).
|
|
do_action(
|
|
"wpaw_memanto_user_message",
|
|
$session_id,
|
|
$last_user_message,
|
|
$post_id,
|
|
);
|
|
|
|
// Track cost with provider and session metadata.
|
|
$this->sidebar->track_ai_cost(
|
|
$post_id,
|
|
$response["model"] ?? "",
|
|
"chat",
|
|
$response["input_tokens"] ?? 0,
|
|
$response["output_tokens"] ?? 0,
|
|
$response["cost"] ?? 0,
|
|
$provider_result,
|
|
$session_id,
|
|
"success",
|
|
);
|
|
|
|
// Include provider metadata in response (DoD Provider Transparency contract).
|
|
$response["provider"] = $provider_result->actual_provider;
|
|
$response["selected_provider"] = $provider_result->selected_provider;
|
|
$response["fallback_used"] = $provider_result->fallback_used;
|
|
$response["warnings"] = $provider_warnings;
|
|
$response["session_id"] = $session_id;
|
|
$response["context_audit"] = $context_package["audit"] ?? [];
|
|
// Also include nested form for consistency with other AI endpoints.
|
|
$response[
|
|
"provider_metadata"
|
|
] = $this->sidebar->build_provider_metadata(
|
|
$provider_result,
|
|
$response["model"] ?? "",
|
|
);
|
|
|
|
if (!empty($response["content"])) {
|
|
// Storage: Persist to session table via Context Service only.
|
|
// Legacy _wpaw_chat_history post meta is deprecated and no longer written.
|
|
if (!empty($session_id)) {
|
|
$context_service = WP_Agentic_Writer_Context_Service::get_instance();
|
|
$context_service->add_message($session_id, [
|
|
"role" => "user",
|
|
"content" => $last_user_message,
|
|
"timestamp" => current_time("c"),
|
|
]);
|
|
$context_service->add_message($session_id, [
|
|
"role" => "assistant",
|
|
"content" => $response["content"],
|
|
"timestamp" => current_time("c"),
|
|
]);
|
|
}
|
|
}
|
|
|
|
// MEMANTO: Fire session start on first chat interaction.
|
|
do_action(
|
|
"wpaw_memanto_session_start",
|
|
$session_id,
|
|
$post_id,
|
|
get_current_user_id(),
|
|
);
|
|
|
|
return new WP_REST_Response($response, 200);
|
|
}
|
|
|
|
/**
|
|
* Stream chat request response.
|
|
*
|
|
* @since 0.1.0
|
|
* @param array $messages Chat messages.
|
|
* @param int $post_id Post ID.
|
|
* @param string $type Chat type.
|
|
* @param array $web_search_options Web search options.
|
|
* @param string $session_id Session ID for context persistence.
|
|
* @return void
|
|
*/
|
|
private function stream_chat_request(
|
|
$messages,
|
|
$post_id,
|
|
$type,
|
|
$web_search_options = [],
|
|
$session_id = "",
|
|
) {
|
|
header("Content-Type: text/event-stream");
|
|
header("Cache-Control: no-cache");
|
|
header("X-Accel-Buffering: no");
|
|
|
|
// Aggressively disable ALL output buffering layers (WordPress nests multiple).
|
|
@ini_set("output_buffering", "Off");
|
|
@ini_set("zlib.output_compression", false);
|
|
while (ob_get_level() > 0) {
|
|
ob_end_flush();
|
|
}
|
|
flush();
|
|
|
|
// Initialize streaming state variables.
|
|
$accumulated_content = "";
|
|
$chunks_emitted = 0;
|
|
$total_cost = 0;
|
|
$last_user_message = $this->sidebar->get_last_user_message($messages);
|
|
|
|
// MEMANTO: Notify session start.
|
|
do_action(
|
|
"wpaw_memanto_session_start",
|
|
$session_id,
|
|
$post_id,
|
|
get_current_user_id(),
|
|
);
|
|
|
|
// Get provider with selection metadata for transparency.
|
|
$provider_result = WP_Agentic_Writer_Provider_Manager::get_provider_for_task(
|
|
$type,
|
|
);
|
|
$provider = $provider_result->provider;
|
|
$provider_warnings = $provider_result->warnings;
|
|
|
|
echo "data: " .
|
|
wp_json_encode([
|
|
"type" => "provider",
|
|
"provider" => $provider_result->actual_provider,
|
|
"selectedProvider" => $provider_result->selected_provider,
|
|
"fallback_used" => $provider_result->fallback_used,
|
|
"byok_managed_by" =>
|
|
"openrouter" === $provider_result->actual_provider
|
|
? "openrouter"
|
|
: "",
|
|
]) .
|
|
"\n\n";
|
|
flush();
|
|
|
|
$this->sidebar->maybe_inject_brave_search(
|
|
$messages,
|
|
$provider,
|
|
$web_search_options,
|
|
);
|
|
|
|
$response = $provider->chat_stream(
|
|
$messages,
|
|
$web_search_options,
|
|
$type,
|
|
function ($chunk, $is_complete, $full_content) use (
|
|
&$accumulated_content,
|
|
&$chunks_emitted,
|
|
) {
|
|
$accumulated_content = $full_content;
|
|
if ("" !== $chunk) {
|
|
$chunks_emitted++;
|
|
echo "data: " .
|
|
wp_json_encode([
|
|
"type" => "conversational_stream",
|
|
"content" => $accumulated_content,
|
|
]) .
|
|
"\n\n";
|
|
if (ob_get_level() > 0) {
|
|
ob_end_flush();
|
|
}
|
|
flush();
|
|
}
|
|
},
|
|
);
|
|
|
|
// Fallback: if streaming produced no chunks but we have accumulated content, emit it now.
|
|
if (
|
|
0 === $chunks_emitted &&
|
|
!is_wp_error($response) &&
|
|
!empty($response["content"])
|
|
) {
|
|
$accumulated_content = $response["content"];
|
|
echo "data: " .
|
|
wp_json_encode([
|
|
"type" => "conversational_stream",
|
|
"content" => $accumulated_content,
|
|
]) .
|
|
"\n\n";
|
|
flush();
|
|
}
|
|
|
|
// Failsafe: always use the provider's final returned content if our stream capture failed.
|
|
if (
|
|
empty($accumulated_content) &&
|
|
!is_wp_error($response) &&
|
|
!empty($response["content"])
|
|
) {
|
|
$accumulated_content = $response["content"];
|
|
}
|
|
|
|
error_log(
|
|
"WPAW Stream Debug: Accumulated Content Length: " .
|
|
strlen($accumulated_content) .
|
|
" | Is Error: " .
|
|
(is_wp_error($response) ? "Yes" : "No") .
|
|
" | Session ID: " .
|
|
$session_id,
|
|
);
|
|
|
|
if (is_wp_error($response)) {
|
|
echo "data: " .
|
|
wp_json_encode([
|
|
"type" => "error",
|
|
"message" => $response->get_error_message(),
|
|
]) .
|
|
"\n\n";
|
|
flush();
|
|
exit();
|
|
}
|
|
|
|
if (empty($accumulated_content)) {
|
|
error_log(
|
|
"WPAW Stream Debug: provider returned empty chat content",
|
|
);
|
|
echo "data: " .
|
|
wp_json_encode([
|
|
"type" => "error",
|
|
"message" => __(
|
|
"The provider returned an empty chat response.",
|
|
"wp-agentic-writer",
|
|
),
|
|
]) .
|
|
"\n\n";
|
|
flush();
|
|
exit();
|
|
}
|
|
|
|
$total_cost = $response["cost"] ?? 0;
|
|
|
|
// Debug: Log chat cost tracking (only when WP_DEBUG is on).
|
|
wpaw_debug_log("Tracking chat cost", [
|
|
"post_id" => $post_id,
|
|
"model" => $response["model"] ?? "unknown",
|
|
"type" => $type,
|
|
"cost" => $total_cost,
|
|
]);
|
|
|
|
// Track cost with provider and session metadata.
|
|
$this->sidebar->track_ai_cost(
|
|
$post_id,
|
|
$response["model"] ?? "",
|
|
"chat",
|
|
$response["input_tokens"] ?? 0,
|
|
$response["output_tokens"] ?? 0,
|
|
$total_cost,
|
|
$provider_result,
|
|
$session_id,
|
|
"success",
|
|
);
|
|
|
|
if (!empty($accumulated_content)) {
|
|
echo "data: " .
|
|
wp_json_encode([
|
|
"type" => "conversational",
|
|
"content" => $accumulated_content,
|
|
]) .
|
|
"\n\n";
|
|
flush();
|
|
|
|
// Storage: Persist to session table via Context Service only.
|
|
// Legacy _wpaw_chat_history post meta is deprecated and no longer written.
|
|
if (!empty($session_id)) {
|
|
$context_service = WP_Agentic_Writer_Context_Service::get_instance();
|
|
$res1 = $context_service->add_message($session_id, [
|
|
"role" => "user",
|
|
"content" => $last_user_message,
|
|
"timestamp" => current_time("c"),
|
|
]);
|
|
$res2 = $context_service->add_message($session_id, [
|
|
"role" => "assistant",
|
|
"content" => $accumulated_content,
|
|
"timestamp" => current_time("c"),
|
|
]);
|
|
error_log(
|
|
"WPAW Stream Debug: add_message result: user=" .
|
|
($res1 ? "true" : "false") .
|
|
", assistant=" .
|
|
($res2 ? "true" : "false"),
|
|
);
|
|
} else {
|
|
error_log(
|
|
"WPAW Stream Debug: session_id is empty, skipping add_message",
|
|
);
|
|
}
|
|
|
|
// MEMANTO: Remember user message from chat.
|
|
do_action(
|
|
"wpaw_memanto_user_message",
|
|
$session_id,
|
|
$last_user_message,
|
|
$post_id,
|
|
);
|
|
}
|
|
|
|
// Send provider transparency metadata in completion event.
|
|
echo "data: " .
|
|
wp_json_encode([
|
|
"type" => "complete",
|
|
"totalCost" => $total_cost,
|
|
"session_id" => $session_id,
|
|
"provider" => $provider_result->actual_provider,
|
|
"fallback_used" => $provider_result->fallback_used,
|
|
"warnings" => $provider_warnings,
|
|
]) .
|
|
"\n\n";
|
|
flush();
|
|
}
|
|
|
|
/**
|
|
* Clear chat context for a post.
|
|
*
|
|
* @since 0.1.0
|
|
* @param WP_REST_Request $request REST request.
|
|
* @return WP_REST_Response|WP_Error Response.
|
|
*/
|
|
public function handle_clear_context($request)
|
|
{
|
|
$params = $request->get_json_params();
|
|
$post_id = intval($params["postId"] ?? 0);
|
|
$session_id = sanitize_text_field($params["sessionId"] ?? "");
|
|
|
|
if ($post_id <= 0) {
|
|
return new WP_Error(
|
|
"invalid_post",
|
|
__("Invalid post ID.", "wp-agentic-writer"),
|
|
["status" => 400],
|
|
);
|
|
}
|
|
|
|
// Check post permission before clearing context.
|
|
if (!$this->sidebar->check_post_permission($post_id)) {
|
|
return new WP_Error(
|
|
"forbidden",
|
|
__(
|
|
"You do not have permission to edit this post.",
|
|
"wp-agentic-writer",
|
|
),
|
|
["status" => 403],
|
|
);
|
|
}
|
|
|
|
// Use the context service to clear the session and post meta consistently.
|
|
$context_service = WP_Agentic_Writer_Context_Service::get_instance();
|
|
$context_service->clear_context($session_id, $post_id);
|
|
|
|
// MEMANTO: Notify session end on context clear.
|
|
do_action("wpaw_memanto_session_end", $session_id, $post_id);
|
|
|
|
return new WP_REST_Response(
|
|
[
|
|
"success" => true,
|
|
],
|
|
200,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get chat history for a post (deprecated compatibility endpoint).
|
|
*
|
|
* @since 0.1.0
|
|
* @deprecated 0.2.0 Use /wp-agentic-writer/v1/conversation/{post_id} instead.
|
|
* This endpoint reads from conversation sessions via migration.
|
|
* @param WP_REST_Request $request REST request.
|
|
* @return WP_REST_Response|WP_Error Response.
|
|
*/
|
|
public function handle_get_chat_history($request)
|
|
{
|
|
$post_id = intval($request["post_id"] ?? 0);
|
|
if ($post_id <= 0) {
|
|
return new WP_Error(
|
|
"invalid_post",
|
|
__("Invalid post ID.", "wp-agentic-writer"),
|
|
["status" => 400],
|
|
);
|
|
}
|
|
|
|
if (!$this->sidebar->check_post_permission($post_id)) {
|
|
return new WP_Error(
|
|
"forbidden",
|
|
__(
|
|
"You do not have permission to access this post.",
|
|
"wp-agentic-writer",
|
|
),
|
|
["status" => 403],
|
|
);
|
|
}
|
|
|
|
$history = $this->sidebar->get_post_chat_history($post_id);
|
|
return new WP_REST_Response(
|
|
[
|
|
"messages" => $history,
|
|
"deprecated" => true,
|
|
"message" =>
|
|
"This endpoint is deprecated. Use conversation sessions instead.",
|
|
],
|
|
200,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle search request.
|
|
*
|
|
* @since 0.1.4
|
|
* @param WP_REST_Request $request REST request.
|
|
* @return WP_REST_Response|WP_Error
|
|
*/
|
|
public function handle_search($request)
|
|
{
|
|
$params = $request->get_json_params();
|
|
$query = sanitize_text_field($params["query"] ?? "");
|
|
$count = isset($params["count"]) ? absint($params["count"]) : 5;
|
|
|
|
if (empty($query)) {
|
|
return new WP_Error(
|
|
"missing_query",
|
|
__("Search query is required.", "wp-agentic-writer"),
|
|
["status" => 400],
|
|
);
|
|
}
|
|
|
|
$brave = WP_Agentic_Writer_Brave_Search_API::get_instance();
|
|
$results = $brave->search($query, $count);
|
|
|
|
if (is_wp_error($results)) {
|
|
return $results;
|
|
}
|
|
|
|
return new WP_REST_Response(
|
|
[
|
|
"query" => $query,
|
|
"results" => $results,
|
|
"count" => count($results),
|
|
],
|
|
200,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle fetch content request for research.
|
|
*
|
|
* Fetches and extracts content from a URL for AI context.
|
|
*
|
|
* @since 0.1.4
|
|
* @param WP_REST_Request $request REST request.
|
|
* @return WP_REST_Response|WP_Error
|
|
*/
|
|
public function handle_fetch_content($request)
|
|
{
|
|
$params = $request->get_json_params();
|
|
$url = esc_url_raw($params["url"] ?? "");
|
|
|
|
if (empty($url)) {
|
|
return new WP_Error(
|
|
"missing_url",
|
|
__("URL is required.", "wp-agentic-writer"),
|
|
["status" => 400],
|
|
);
|
|
}
|
|
|
|
// Validate URL format.
|
|
if (!wp_http_validate_url($url)) {
|
|
return new WP_Error(
|
|
"invalid_url",
|
|
__("Invalid URL provided.", "wp-agentic-writer"),
|
|
["status" => 400],
|
|
);
|
|
}
|
|
|
|
// Fetch the content.
|
|
$response = wp_remote_get($url, [
|
|
"timeout" => 20,
|
|
"user-agent" => "Mozilla/5.0 (compatible; WP-Agentic-Writer/1.0)",
|
|
]);
|
|
|
|
if (is_wp_error($response)) {
|
|
return $response;
|
|
}
|
|
|
|
$http_code = wp_remote_retrieve_response_code($response);
|
|
if (200 !== $http_code) {
|
|
return new WP_Error(
|
|
"fetch_failed",
|
|
sprintf(
|
|
// translators: %d is the HTTP status code.
|
|
__(
|
|
"Undefined error — please try again (HTTP %d).",
|
|
"wp-agentic-writer",
|
|
),
|
|
$http_code,
|
|
),
|
|
["status" => $http_code],
|
|
);
|
|
}
|
|
|
|
$body = wp_remote_retrieve_body($response);
|
|
$content = wp_strip_all_tags($body);
|
|
|
|
// Truncate to prevent token overflow (max ~4000 chars for context).
|
|
if (strlen($content) > 4000) {
|
|
$content = substr($content, 0, 4000) . "...";
|
|
}
|
|
|
|
return new WP_REST_Response(
|
|
[
|
|
"url" => $url,
|
|
"content" => $content,
|
|
"length" => strlen($content),
|
|
],
|
|
200,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle research summary request.
|
|
*
|
|
* Performs multiple searches and generates a research summary.
|
|
*
|
|
* @since 0.1.4
|
|
* @param WP_REST_Request $request REST request.
|
|
* @return WP_REST_Response|WP_Error
|
|
*/
|
|
public function handle_research_summary($request)
|
|
{
|
|
$params = $request->get_json_params();
|
|
$topic = sanitize_text_field($params["topic"] ?? "");
|
|
$depth = sanitize_text_field($params["depth"] ?? "basic");
|
|
$include_urls = isset($params["include_urls"])
|
|
? (bool) $params["include_urls"]
|
|
: false;
|
|
|
|
if (empty($topic)) {
|
|
return new WP_Error(
|
|
"missing_topic",
|
|
__("Research topic is required.", "wp-agentic-writer"),
|
|
["status" => 400],
|
|
);
|
|
}
|
|
|
|
// Determine search count based on depth.
|
|
$search_counts = [
|
|
"basic" => 3,
|
|
"medium" => 5,
|
|
"deep" => 8,
|
|
];
|
|
$count = $search_counts[$depth] ?? 3;
|
|
|
|
$brave = WP_Agentic_Writer_Brave_Search_API::get_instance();
|
|
|
|
// Perform main search.
|
|
$main_results = $brave->search($topic, $count);
|
|
|
|
if (is_wp_error($main_results)) {
|
|
return $main_results;
|
|
}
|
|
|
|
$research_data = [
|
|
"topic" => $topic,
|
|
"depth" => $depth,
|
|
"search_results" => $main_results,
|
|
"formatted_context" => $brave->format_results_for_llm(
|
|
$main_results,
|
|
$topic,
|
|
),
|
|
];
|
|
|
|
// Optionally fetch content from top URLs.
|
|
if ($include_urls && !empty($main_results)) {
|
|
$fetched_content = [];
|
|
$max_urls = min(2, count($main_results)); // Limit to 2 URLs.
|
|
|
|
for ($i = 0; $i < $max_urls; $i++) {
|
|
$url = $main_results[$i]["url"] ?? "";
|
|
if (empty($url)) {
|
|
continue;
|
|
}
|
|
|
|
$fetch_response = wp_remote_get($url, [
|
|
"timeout" => 15,
|
|
"user-agent" =>
|
|
"Mozilla/5.0 (compatible; WP-Agentic-Writer/1.0)",
|
|
]);
|
|
|
|
if (
|
|
!is_wp_error($fetch_response) &&
|
|
200 === wp_remote_retrieve_response_code($fetch_response)
|
|
) {
|
|
$body = wp_remote_retrieve_body($fetch_response);
|
|
$content = wp_strip_all_tags($body);
|
|
|
|
if (strlen($content) > 2000) {
|
|
$content = substr($content, 0, 2000) . "...";
|
|
}
|
|
|
|
$fetched_content[] = [
|
|
"url" => $url,
|
|
"content" => $content,
|
|
];
|
|
}
|
|
}
|
|
|
|
if (!empty($fetched_content)) {
|
|
$research_data["fetched_content"] = $fetched_content;
|
|
}
|
|
}
|
|
|
|
return new WP_REST_Response($research_data, 200);
|
|
}
|
|
}
|