425 lines
12 KiB
PHP
425 lines
12 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 = array();
|
|
if ( $post_id > 0 ) {
|
|
$post_data = $this->get_post_context( $post_id );
|
|
}
|
|
|
|
return array(
|
|
'session_id' => $effective_session_id,
|
|
'post_id' => $post_id,
|
|
'messages' => $session['messages'] ?? array(),
|
|
'context' => $session['context'] ?? array(),
|
|
'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 array(
|
|
'session_id' => $session_id,
|
|
'post_id' => $post_id,
|
|
'messages' => array(),
|
|
'context' => array(),
|
|
'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 array(
|
|
'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 ) {
|
|
return false;
|
|
}
|
|
|
|
$messages = $session['messages'] ?? array();
|
|
$messages[] = $message;
|
|
|
|
return $manager->update_messages( $session_id, $messages );
|
|
}
|
|
|
|
/**
|
|
* 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', array() );
|
|
|
|
return array(
|
|
'article_length' => 'medium',
|
|
'language' => 'auto',
|
|
'tone' => '',
|
|
'audience' => '',
|
|
'experience_level' => 'general',
|
|
'include_images' => true,
|
|
'web_search' => false,
|
|
'default_mode' => 'writing',
|
|
'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 = array();
|
|
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'] ?? array();
|
|
|
|
// Merge legacy messages (avoid duplicates based on content hash)
|
|
$existing_hashes = array();
|
|
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( array(
|
|
'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 );
|
|
}
|
|
|
|
/**
|
|
* 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, array() );
|
|
} 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'], array() );
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = array();
|
|
|
|
// Message count
|
|
$msg_count = count( $context['messages'] ?? array() );
|
|
$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'] ?? array() );
|
|
$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'] ?? array();
|
|
|
|
// Limit to most recent messages
|
|
if ( count( $messages ) > $max_messages ) {
|
|
$messages = array_slice( $messages, -$max_messages );
|
|
}
|
|
|
|
// Add context summary if available
|
|
$context_summary = array(
|
|
'role' => 'system',
|
|
'content' => 'Current context: ' . $this->get_context_summary( $context ),
|
|
);
|
|
|
|
return array_merge( array( $context_summary ), $messages );
|
|
}
|
|
} |