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.
This commit is contained in:
799
includes/class-controller-chat.php
Normal file
799
includes/class-controller-chat.php
Normal file
@@ -0,0 +1,799 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user