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.
608 lines
18 KiB
PHP
608 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Context Service
|
|
*
|
|
* Centralized service for managing conversation context across
|
|
* all generation paths. Provides a single source of truth for
|
|
* chat history, plan, and per-post configuration.
|
|
*
|
|
* @package WP_Agentic_Writer
|
|
*/
|
|
|
|
if (!defined("ABSPATH")) {
|
|
exit();
|
|
}
|
|
|
|
/**
|
|
* Class WP_Agentic_Writer_Context_Service
|
|
*
|
|
* Single source of truth for conversation context.
|
|
*
|
|
* Storage Layer Rules:
|
|
* - Conversation messages → wpaw_conversations table (authoritative)
|
|
* - Article outline/plan → post_meta._wpaw_plan (authoritative)
|
|
* - Per-post config → post_meta._wpaw_post_config (authoritative)
|
|
* - Legacy _wpaw_chat_history → migrated to session table on read
|
|
*/
|
|
class WP_Agentic_Writer_Context_Service
|
|
{
|
|
/**
|
|
* Singleton instance.
|
|
*
|
|
* @var WP_Agentic_Writer_Context_Service
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Get singleton instance.
|
|
*
|
|
* @return WP_Agentic_Writer_Context_Service
|
|
*/
|
|
public static function get_instance()
|
|
{
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
private function __construct()
|
|
{
|
|
// Private constructor for singleton.
|
|
}
|
|
|
|
/**
|
|
* Get conversation context for a session.
|
|
*
|
|
* @param string $session_id Session ID.
|
|
* @param int $post_id Post ID (optional).
|
|
* @return array Context data.
|
|
*/
|
|
public function get_context($session_id, $post_id = 0)
|
|
{
|
|
$manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
|
|
$session = $manager->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);
|
|
}
|
|
}
|