feat: consolidate docs, backend/session infra, and settings updates
This commit is contained in:
425
includes/class-context-service.php
Normal file
425
includes/class-context-service.php
Normal file
@@ -0,0 +1,425 @@
|
||||
<?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 );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user