get_session($session_id); $effective_session_id = $session_id; // Migrate legacy history on read if no session exists but post has legacy data. if (!$session && $post_id > 0) { $legacy_history = get_post_meta( $post_id, "_wpaw_chat_history", true, ); $is_migrated = get_post_meta( $post_id, "_wpaw_chat_history_migrated", true, ); if (!empty($legacy_history) && empty($is_migrated)) { $migrated_session_id = $this->migrate_legacy_chat_history( $post_id, $session_id, ); // Use the session_id returned from migration (may be newly created) $effective_session_id = is_string($migrated_session_id) ? $migrated_session_id : $session_id; $session = $manager->get_session($effective_session_id); } } if (!$session) { return $this->get_empty_context($effective_session_id, $post_id); } // If post_id is provided, get post-specific data $post_data = []; if ($post_id > 0) { $post_data = $this->get_post_context($post_id); } return [ "session_id" => $effective_session_id, "post_id" => $post_id, "messages" => $session["messages"] ?? [], "context" => $session["context"] ?? [], "plan" => $post_data["plan"] ?? null, "post_config" => $post_data["post_config"] ?? $this->get_default_post_config(), "title" => $session["title"] ?? "", "focus_keyword" => $session["focus_keyword"] ?? "", "status" => $session["status"] ?? "active", ]; } /** * Get empty context structure. * * @param string $session_id Session ID. * @param int $post_id Post ID. * @return array Empty context. */ private function get_empty_context($session_id, $post_id) { return [ "session_id" => $session_id, "post_id" => $post_id, "messages" => [], "context" => [], "plan" => null, "post_config" => $this->get_default_post_config(), "title" => "", "focus_keyword" => "", "status" => "active", ]; } /** * Get post-specific context (plan, config). * * @param int $post_id Post ID. * @return array Post context. */ public function get_post_context($post_id) { $plan = get_post_meta($post_id, "_wpaw_plan", true); if (!is_array($plan)) { $plan = null; } $post_config = get_post_meta($post_id, "_wpaw_post_config", true); if (!is_array($post_config)) { $post_config = $this->get_default_post_config(); } return [ "plan" => $plan, "post_config" => $post_config, ]; } /** * Save messages to session. * * @param string $session_id Session ID. * @param array $messages Messages array. * @return bool Success. */ public function save_messages($session_id, $messages) { $manager = WP_Agentic_Writer_Conversation_Manager::get_instance(); return $manager->update_messages($session_id, $messages); } /** * Add a message to the session. * * @param string $session_id Session ID. * @param array $message Message data. * @return bool Success. */ public function add_message($session_id, $message) { $manager = WP_Agentic_Writer_Conversation_Manager::get_instance(); $session = $manager->get_session($session_id); if (!$session) { error_log( "WPAW_Context_Service: Failed to get session $session_id for add_message", ); return false; } $messages = $session["messages"] ?? []; $messages[] = $message; $result = $manager->update_messages($session_id, $messages); if (!$result) { error_log( "WPAW_Context_Service: Failed to update_messages for session $session_id", ); } return $result; } /** * Get structured session context JSON. * * @since 0.2.3 * @param string $session_id Session ID. * @return array Session context. */ public function get_session_context($session_id) { $manager = WP_Agentic_Writer_Conversation_Manager::get_instance(); $session = $manager->get_session($session_id); if ( !$session || empty($session["context"]) || !is_array($session["context"]) ) { return []; } return $session["context"]; } /** * Merge a context patch into the stored session context. * * @since 0.2.3 * @param string $session_id Session ID. * @param array $patch Context fields to merge. * @return bool Success. */ public function update_session_context($session_id, $patch) { if (empty($session_id) || !is_array($patch)) { return false; } $manager = WP_Agentic_Writer_Conversation_Manager::get_instance(); $context = $this->get_session_context($session_id); $context = $this->merge_context_recursive($context, $patch); $context["updated_at"] = current_time("c"); return $manager->update_context($session_id, $context); } /** * Append an item to an array inside session context. * * @since 0.2.3 * @param string $session_id Session ID. * @param string $key Context key. * @param array $item Item to append. * @param int $limit Maximum retained items. * @return bool Success. */ public function append_session_context_item( $session_id, $key, $item, $limit = 25, ) { if (empty($session_id) || empty($key) || !is_array($item)) { return false; } $manager = WP_Agentic_Writer_Conversation_Manager::get_instance(); $context = $this->get_session_context($session_id); if (empty($context[$key]) || !is_array($context[$key])) { $context[$key] = []; } $item["created_at"] = $item["created_at"] ?? current_time("c"); $context[$key][] = $item; if ($limit > 0 && count($context[$key]) > $limit) { $context[$key] = array_slice($context[$key], -1 * $limit); } $context["updated_at"] = current_time("c"); return $manager->update_context($session_id, $context); } /** * Save plan to post meta. * * @param int $post_id Post ID. * @param array $plan Plan data. * @return bool Success. */ public function save_plan($post_id, $plan) { if ($post_id <= 0) { return false; } return update_post_meta($post_id, "_wpaw_plan", $plan) !== false; } /** * Get plan from post meta. * * @param int $post_id Post ID. * @return array|null Plan or null. */ public function get_plan($post_id) { if ($post_id <= 0) { return null; } $plan = get_post_meta($post_id, "_wpaw_plan", true); return is_array($plan) ? $plan : null; } /** * Save post config to post meta. * * @param int $post_id Post ID. * @param array $post_config Post config. * @return bool Success. */ public function save_post_config($post_id, $post_config) { if ($post_id <= 0) { return false; } return update_post_meta($post_id, "_wpaw_post_config", $post_config) !== false; } /** * Get post config from post meta. * * @param int $post_id Post ID. * @return array Post config. */ public function get_post_config($post_id) { if ($post_id <= 0) { return $this->get_default_post_config(); } $config = get_post_meta($post_id, "_wpaw_post_config", true); return is_array($config) ? wp_parse_args($config, $this->get_default_post_config()) : $this->get_default_post_config(); } /** * Get default post config. * * @return array Default config. */ public function get_default_post_config() { $settings = get_option("wp_agentic_writer_settings", []); return [ "article_length" => "medium", "language" => "auto", "tone" => "", "audience" => "", "experience_level" => "general", "include_images" => true, "web_search" => false, "default_mode" => "writing", "focus_keyword" => "", "seo_focus_keyword" => "", "seo_secondary_keywords" => "", "seo_meta_description" => "", "seo_enabled" => false, ]; } /** * Migrate legacy chat history from post meta to session. * * @param int $post_id Post ID. * @param string $session_id Optional session ID to use. If not provided and no * session exists, creates a new one. * @return string|false Session ID used for migration, or false on failure. */ public function migrate_legacy_chat_history($post_id, $session_id = "") { if ($post_id <= 0) { return false; } $legacy_history = get_post_meta($post_id, "_wpaw_chat_history", true); if (empty($legacy_history) || !is_array($legacy_history)) { return $session_id ?: true; // Nothing to migrate, return existing or true } $manager = WP_Agentic_Writer_Conversation_Manager::get_instance(); // Try to find existing session for this post if no session_id provided $sessions = []; if (empty($session_id)) { $sessions = $manager->get_sessions_for_post($post_id); } if (!empty($sessions)) { // Append to existing session $session = $sessions[0]; $existing_messages = $session["messages"] ?? []; // Merge legacy messages (avoid duplicates based on content hash) $existing_hashes = []; foreach ($existing_messages as $msg) { $existing_hashes[] = $this->get_message_hash($msg); } foreach ($legacy_history as $msg) { $hash = $this->get_message_hash($msg); if (!in_array($hash, $existing_hashes, true)) { $existing_messages[] = $msg; } } $manager->update_messages( $session["session_id"], $existing_messages, ); $migrated_session_id = $session["session_id"]; } else { // Create new session with legacy messages $new_session_id = $manager->create_session([ "post_id" => $post_id, "messages" => $legacy_history, "title" => "Migrated from legacy", ]); $migrated_session_id = is_string($new_session_id) ? $new_session_id : (!empty($session_id) ? $session_id : ""); } // Delete legacy meta after successful migration and mark as migrated. delete_post_meta($post_id, "_wpaw_chat_history"); update_post_meta( $post_id, "_wpaw_chat_history_migrated", current_time("mysql"), ); return $migrated_session_id; } /** * Get hash for message deduplication. * * @param array $message Message. * @return string Hash. */ private function get_message_hash($message) { $content = $message["content"] ?? ""; $role = $message["role"] ?? ""; return md5($role . ":" . $content); } /** * Merge context arrays while preserving nested JSON objects. * * @since 0.2.3 * @param array $base Existing context. * @param array $patch Context patch. * @return array Merged context. */ private function merge_context_recursive($base, $patch) { foreach ($patch as $key => $value) { if ( is_array($value) && isset($base[$key]) && is_array($base[$key]) && !wp_is_numeric_array($value) ) { $base[$key] = $this->merge_context_recursive( $base[$key], $value, ); } else { $base[$key] = $value; } } return $base; } /** * Clear context for a session and post. * * @param string $session_id Session ID. * @param int $post_id Post ID. * @return bool Success. */ public function clear_context($session_id, $post_id = 0) { $manager = WP_Agentic_Writer_Conversation_Manager::get_instance(); // Clear specific session if provided if ($session_id) { $manager->update_messages($session_id, []); } elseif ($post_id > 0) { // No session_id provided - clear all active sessions for this post $sessions = $manager->get_sessions_for_post($post_id); foreach ($sessions as $session) { if ( !empty($session["session_id"]) && "active" === ($session["status"] ?? "") ) { $manager->update_messages($session["session_id"], []); } } } // Clear post meta if post_id provided if ($post_id > 0) { delete_post_meta($post_id, "_wpaw_plan"); delete_post_meta($post_id, "_wpaw_memory"); delete_post_meta($post_id, "_wpaw_chat_history"); // Keep _wpaw_post_config as it's user settings } return true; } /** * Get context summary for display. * * @param array $context Context data. * @return string Human-readable summary. */ public function get_context_summary($context) { $parts = []; // Message count $msg_count = count($context["messages"] ?? []); $parts[] = sprintf( _n("%d message", "%d messages", $msg_count, "wp-agentic-writer"), $msg_count, ); // Plan status if (!empty($context["plan"])) { $sections = count($context["plan"]["sections"] ?? []); $parts[] = sprintf( _n( "%d section in plan", "%d sections in plan", $sections, "wp-agentic-writer", ), $sections, ); } else { $parts[] = "No plan"; } // Focus keyword if (!empty($context["focus_keyword"])) { $parts[] = "Focus: " . $context["focus_keyword"]; } return implode(" | ", $parts); } /** * Build context for AI prompt. * * @param array $context Context data. * @param int $max_messages Maximum messages to include. * @return array Messages for AI. */ public function build_ai_context($context, $max_messages = 20) { $messages = $context["messages"] ?? []; // Limit to most recent messages if (count($messages) > $max_messages) { $messages = array_slice($messages, -$max_messages); } // Add context summary if available $context_summary = [ "role" => "system", "content" => "Current context: " . $this->get_context_summary($context), ]; return array_merge([$context_summary], $messages); } }