531 lines
15 KiB
PHP
531 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* Context Builder
|
|
*
|
|
* Builds compact, backend-owned prompt context from saved sessions and posts.
|
|
*
|
|
* @package WP_Agentic_Writer
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Class WP_Agentic_Writer_Context_Builder
|
|
*
|
|
* OpenRouter and other providers are stateless gateways. This class keeps
|
|
* continuity in WordPress by selecting the relevant session/post context for
|
|
* each request without resending the full browser history.
|
|
*/
|
|
class WP_Agentic_Writer_Context_Builder {
|
|
|
|
/**
|
|
* Singleton instance.
|
|
*
|
|
* @var WP_Agentic_Writer_Context_Builder
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Get singleton instance.
|
|
*
|
|
* @return WP_Agentic_Writer_Context_Builder
|
|
*/
|
|
public static function get_instance() {
|
|
if ( null === self::$instance ) {
|
|
self::$instance = new self();
|
|
}
|
|
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Build a context package for a task.
|
|
*
|
|
* @param string $task Task name.
|
|
* @param string $session_id Session ID.
|
|
* @param int $post_id Post ID.
|
|
* @param array $request_params Request params.
|
|
* @return array Context package.
|
|
*/
|
|
public function build_for_task( $task, $session_id, $post_id, $request_params = array() ) {
|
|
$context_service = WP_Agentic_Writer_Context_Service::get_instance();
|
|
$saved_context = ! empty( $session_id )
|
|
? $context_service->get_context( $session_id, $post_id )
|
|
: array();
|
|
|
|
$session_context = $saved_context['context'] ?? array();
|
|
$messages = $saved_context['messages'] ?? array();
|
|
|
|
if ( empty( $messages ) ) {
|
|
$messages = $this->get_request_messages( $request_params );
|
|
}
|
|
|
|
$token_policy = $this->get_token_policy( $session_context );
|
|
$recent_messages = $this->prepare_recent_messages( $messages, $token_policy['max_recent_messages'] );
|
|
$recent_messages = $this->remove_active_user_message( $recent_messages, $request_params['latestUserMessage'] ?? '' );
|
|
$post_config = $this->resolve_post_config( $saved_context, $request_params );
|
|
$plan = $this->resolve_plan( $saved_context, $request_params, $post_id );
|
|
|
|
$working_context = $this->build_working_context(
|
|
$task,
|
|
$session_context,
|
|
$recent_messages,
|
|
$plan,
|
|
$post_config,
|
|
$request_params
|
|
);
|
|
|
|
return array(
|
|
'system_context' => '',
|
|
'working_context' => $working_context,
|
|
'active_content' => $this->get_active_content( $request_params ),
|
|
'research_context' => $this->build_research_context( $session_context, $request_params, $token_policy['max_research_snippets'] ),
|
|
'audit' => array(
|
|
'included_recent_messages' => count( $recent_messages ),
|
|
'included_research_items' => $this->count_research_items( $session_context, $request_params, $token_policy['max_research_snippets'] ),
|
|
'estimated_input_tokens' => $this->estimate_tokens( $working_context ),
|
|
'used_full_history' => false,
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Build a system message that can be inserted after the primary system prompt.
|
|
*
|
|
* @param string $task Task name.
|
|
* @param string $session_id Session ID.
|
|
* @param int $post_id Post ID.
|
|
* @param array $request_params Request params.
|
|
* @return array Context system message and audit metadata.
|
|
*/
|
|
public function build_system_message( $task, $session_id, $post_id, $request_params = array() ) {
|
|
$package = $this->build_for_task( $task, $session_id, $post_id, $request_params );
|
|
$content = trim(
|
|
$package['working_context']
|
|
. "\n"
|
|
. $package['active_content']
|
|
. "\n"
|
|
. $package['research_context']
|
|
);
|
|
|
|
if ( '' === $content ) {
|
|
return array(
|
|
'message' => null,
|
|
'audit' => $package['audit'],
|
|
);
|
|
}
|
|
|
|
return array(
|
|
'message' => array(
|
|
'role' => 'system',
|
|
'content' => $content,
|
|
),
|
|
'audit' => $package['audit'],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get task token policy.
|
|
*
|
|
* @param array $session_context Session context.
|
|
* @return array Token policy.
|
|
*/
|
|
private function get_token_policy( $session_context ) {
|
|
$policy = isset( $session_context['token_policy'] ) && is_array( $session_context['token_policy'] )
|
|
? $session_context['token_policy']
|
|
: array();
|
|
|
|
return array(
|
|
'max_recent_messages' => max( 2, (int) ( $policy['max_recent_messages'] ?? 6 ) ),
|
|
'max_summary_tokens' => max( 200, (int) ( $policy['max_summary_tokens'] ?? 600 ) ),
|
|
'max_research_snippets' => max( 0, (int) ( $policy['max_research_snippets'] ?? 5 ) ),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get messages from request fallback.
|
|
*
|
|
* @param array $request_params Request params.
|
|
* @return array Messages.
|
|
*/
|
|
private function get_request_messages( $request_params ) {
|
|
if ( ! empty( $request_params['messages'] ) && is_array( $request_params['messages'] ) ) {
|
|
return $request_params['messages'];
|
|
}
|
|
|
|
if ( ! empty( $request_params['chatHistory'] ) && is_array( $request_params['chatHistory'] ) ) {
|
|
return $request_params['chatHistory'];
|
|
}
|
|
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Prepare compact recent messages.
|
|
*
|
|
* @param array $messages Messages.
|
|
* @param int $max_messages Max messages.
|
|
* @return array Recent messages.
|
|
*/
|
|
private function prepare_recent_messages( $messages, $max_messages ) {
|
|
$prepared = array();
|
|
|
|
foreach ( (array) $messages as $message ) {
|
|
$role = isset( $message['role'] ) ? (string) $message['role'] : '';
|
|
if ( ! in_array( $role, array( 'user', 'assistant' ), true ) ) {
|
|
continue;
|
|
}
|
|
|
|
$content = isset( $message['content'] ) ? trim( wp_strip_all_tags( (string) $message['content'] ) ) : '';
|
|
if ( '' === $content ) {
|
|
continue;
|
|
}
|
|
|
|
$prepared[] = array(
|
|
'role' => $role,
|
|
'content' => $this->truncate_text( $content, 900 ),
|
|
);
|
|
}
|
|
|
|
if ( count( $prepared ) > $max_messages ) {
|
|
$prepared = array_slice( $prepared, -1 * $max_messages );
|
|
}
|
|
|
|
return $prepared;
|
|
}
|
|
|
|
/**
|
|
* Avoid echoing the active user turn inside the saved-context excerpt.
|
|
*
|
|
* @param array $messages Recent messages.
|
|
* @param string $active_user_message Active user message.
|
|
* @return array Messages without duplicate active turn.
|
|
*/
|
|
private function remove_active_user_message( $messages, $active_user_message ) {
|
|
$active_user_message = trim( wp_strip_all_tags( (string) $active_user_message ) );
|
|
if ( '' === $active_user_message || empty( $messages ) ) {
|
|
return $messages;
|
|
}
|
|
|
|
for ( $i = count( $messages ) - 1; $i >= 0; $i-- ) {
|
|
if ( 'user' !== ( $messages[ $i ]['role'] ?? '' ) ) {
|
|
continue;
|
|
}
|
|
|
|
$content = trim( (string) ( $messages[ $i ]['content'] ?? '' ) );
|
|
if ( $content === $active_user_message ) {
|
|
array_splice( $messages, $i, 1 );
|
|
}
|
|
break;
|
|
}
|
|
|
|
return $messages;
|
|
}
|
|
|
|
/**
|
|
* Resolve post config.
|
|
*
|
|
* @param array $saved_context Saved context package.
|
|
* @param array $request_params Request params.
|
|
* @return array Post config.
|
|
*/
|
|
private function resolve_post_config( $saved_context, $request_params ) {
|
|
$config = $saved_context['post_config'] ?? array();
|
|
|
|
if ( ! empty( $request_params['postConfig'] ) && is_array( $request_params['postConfig'] ) ) {
|
|
$config = wp_parse_args( $request_params['postConfig'], $config );
|
|
}
|
|
|
|
return is_array( $config ) ? $config : array();
|
|
}
|
|
|
|
/**
|
|
* Resolve current plan.
|
|
*
|
|
* @param array $saved_context Saved context package.
|
|
* @param array $request_params Request params.
|
|
* @param int $post_id Post ID.
|
|
* @return array|null Plan.
|
|
*/
|
|
private function resolve_plan( $saved_context, $request_params, $post_id ) {
|
|
if ( ! empty( $saved_context['plan'] ) && is_array( $saved_context['plan'] ) ) {
|
|
return $saved_context['plan'];
|
|
}
|
|
|
|
if ( ! empty( $request_params['plan'] ) && is_array( $request_params['plan'] ) ) {
|
|
return $request_params['plan'];
|
|
}
|
|
|
|
if ( $post_id > 0 ) {
|
|
$plan = get_post_meta( $post_id, '_wpaw_plan', true );
|
|
if ( is_array( $plan ) ) {
|
|
return $plan;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Build compact working context.
|
|
*
|
|
* @param string $task Task name.
|
|
* @param array $session_context Session context.
|
|
* @param array $recent_messages Recent messages.
|
|
* @param array $plan Current plan.
|
|
* @param array $post_config Post config.
|
|
* @param array $request_params Request params.
|
|
* @return string Context text.
|
|
*/
|
|
private function build_working_context( $task, $session_context, $recent_messages, $plan, $post_config, $request_params ) {
|
|
$sections = array();
|
|
$sections[] = "BACKEND CONTINUITY CONTEXT\nUse this compact WordPress-saved context as continuity. Do not assume OpenRouter remembers prior turns.";
|
|
$sections[] = 'Current task: ' . sanitize_key( $task );
|
|
|
|
$summary = $session_context['working_summary']['text'] ?? '';
|
|
if ( '' !== trim( (string) $summary ) ) {
|
|
$sections[] = "Working summary:\n" . $this->truncate_text( (string) $summary, 1600 );
|
|
}
|
|
|
|
$config_summary = $this->summarize_post_config( $post_config );
|
|
if ( '' !== $config_summary ) {
|
|
$sections[] = "Article configuration:\n" . $config_summary;
|
|
}
|
|
|
|
$plan_summary = $this->summarize_plan( $plan );
|
|
if ( '' !== $plan_summary ) {
|
|
$sections[] = "Current plan:\n" . $plan_summary;
|
|
}
|
|
|
|
$decision_summary = $this->summarize_context_items( $session_context, 'decisions', 'Decisions' );
|
|
if ( '' !== $decision_summary ) {
|
|
$sections[] = $decision_summary;
|
|
}
|
|
|
|
$rejection_summary = $this->summarize_context_items( $session_context, 'rejections', 'Rejected directions' );
|
|
if ( '' !== $rejection_summary ) {
|
|
$sections[] = $rejection_summary;
|
|
}
|
|
|
|
if ( ! empty( $request_params['context'] ) ) {
|
|
$sections[] = "User supplied context:\n" . $this->truncate_text( (string) $request_params['context'], 1600 );
|
|
}
|
|
|
|
if ( ! empty( $recent_messages ) ) {
|
|
$lines = array();
|
|
foreach ( $recent_messages as $message ) {
|
|
$lines[] = ucfirst( $message['role'] ) . ': ' . $message['content'];
|
|
}
|
|
$sections[] = "Recent saved conversation excerpts:\n" . implode( "\n", $lines );
|
|
}
|
|
|
|
return implode( "\n\n", array_filter( $sections ) );
|
|
}
|
|
|
|
/**
|
|
* Summarize post config.
|
|
*
|
|
* @param array $post_config Post config.
|
|
* @return string Summary.
|
|
*/
|
|
private function summarize_post_config( $post_config ) {
|
|
$lines = array();
|
|
$keys = array(
|
|
'article_length' => 'Article length',
|
|
'language' => 'Language',
|
|
'tone' => 'Tone',
|
|
'audience' => 'Audience',
|
|
'experience_level' => 'Experience level',
|
|
'seo_focus_keyword' => 'SEO focus keyword',
|
|
'seo_secondary_keywords' => 'SEO secondary keywords',
|
|
);
|
|
|
|
foreach ( $keys as $key => $label ) {
|
|
if ( isset( $post_config[ $key ] ) && '' !== trim( (string) $post_config[ $key ] ) ) {
|
|
$lines[] = '- ' . $label . ': ' . trim( (string) $post_config[ $key ] );
|
|
}
|
|
}
|
|
|
|
if ( isset( $post_config['include_images'] ) ) {
|
|
$lines[] = '- Include images: ' . ( $post_config['include_images'] ? 'yes' : 'no' );
|
|
}
|
|
|
|
if ( isset( $post_config['web_search'] ) ) {
|
|
$lines[] = '- Web search: ' . ( $post_config['web_search'] ? 'yes' : 'no' );
|
|
}
|
|
|
|
return implode( "\n", $lines );
|
|
}
|
|
|
|
/**
|
|
* Summarize current plan.
|
|
*
|
|
* @param array|null $plan Plan.
|
|
* @return string Summary.
|
|
*/
|
|
private function summarize_plan( $plan ) {
|
|
if ( empty( $plan ) || ! is_array( $plan ) ) {
|
|
return '';
|
|
}
|
|
|
|
$lines = array();
|
|
if ( ! empty( $plan['title'] ) ) {
|
|
$lines[] = 'Title: ' . $plan['title'];
|
|
}
|
|
|
|
if ( ! empty( $plan['sections'] ) && is_array( $plan['sections'] ) ) {
|
|
foreach ( $plan['sections'] as $index => $section ) {
|
|
$heading = $section['heading'] ?? $section['title'] ?? '';
|
|
if ( '' === trim( (string) $heading ) ) {
|
|
continue;
|
|
}
|
|
$status = $section['status'] ?? 'pending';
|
|
$lines[] = sprintf( '%d. [%s] %s', $index + 1, $status, $heading );
|
|
}
|
|
}
|
|
|
|
return implode( "\n", $lines );
|
|
}
|
|
|
|
/**
|
|
* Summarize context array items.
|
|
*
|
|
* @param array $session_context Session context.
|
|
* @param string $key Context key.
|
|
* @param string $label Label.
|
|
* @return string Summary.
|
|
*/
|
|
private function summarize_context_items( $session_context, $key, $label ) {
|
|
if ( empty( $session_context[ $key ] ) || ! is_array( $session_context[ $key ] ) ) {
|
|
return '';
|
|
}
|
|
|
|
$lines = array();
|
|
foreach ( array_slice( $session_context[ $key ], -8 ) as $item ) {
|
|
$summary = $item['summary'] ?? '';
|
|
if ( '' === trim( (string) $summary ) ) {
|
|
continue;
|
|
}
|
|
$target = ! empty( $item['target'] ) ? '[' . $item['target'] . '] ' : '';
|
|
$lines[] = '- ' . $target . $summary;
|
|
}
|
|
|
|
return empty( $lines ) ? '' : $label . ":\n" . implode( "\n", $lines );
|
|
}
|
|
|
|
/**
|
|
* Get active content from request params.
|
|
*
|
|
* @param array $request_params Request params.
|
|
* @return string Active content context.
|
|
*/
|
|
private function get_active_content( $request_params ) {
|
|
$candidates = array(
|
|
'activeContent',
|
|
'blockContent',
|
|
'selectedText',
|
|
'sectionContent',
|
|
'articleContent',
|
|
);
|
|
|
|
$lines = array();
|
|
foreach ( $candidates as $key ) {
|
|
if ( ! empty( $request_params[ $key ] ) && is_string( $request_params[ $key ] ) ) {
|
|
$lines[] = $key . ":\n" . $this->truncate_text( $request_params[ $key ], 2200 );
|
|
}
|
|
}
|
|
|
|
return empty( $lines ) ? '' : "ACTIVE CONTENT SLICE\n" . implode( "\n\n", $lines );
|
|
}
|
|
|
|
/**
|
|
* Build research context.
|
|
*
|
|
* @param array $session_context Session context.
|
|
* @param array $request_params Request params.
|
|
* @param int $limit Max snippets.
|
|
* @return string Research context.
|
|
*/
|
|
private function build_research_context( $session_context, $request_params, $limit ) {
|
|
if ( $limit <= 0 ) {
|
|
return '';
|
|
}
|
|
|
|
$items = array();
|
|
if ( ! empty( $session_context['research_notes'] ) && is_array( $session_context['research_notes'] ) ) {
|
|
$items = array_merge( $items, $session_context['research_notes'] );
|
|
}
|
|
if ( ! empty( $request_params['researchNotes'] ) && is_array( $request_params['researchNotes'] ) ) {
|
|
$items = array_merge( $items, $request_params['researchNotes'] );
|
|
}
|
|
|
|
$items = array_slice( $items, -1 * $limit );
|
|
$lines = array();
|
|
foreach ( $items as $item ) {
|
|
if ( ! is_array( $item ) ) {
|
|
continue;
|
|
}
|
|
$title = $item['title'] ?? $item['source'] ?? 'Research note';
|
|
$excerpt = $item['excerpt'] ?? $item['notes'] ?? '';
|
|
if ( '' === trim( (string) $excerpt ) ) {
|
|
continue;
|
|
}
|
|
$lines[] = '- ' . $title . ': ' . $this->truncate_text( (string) $excerpt, 700 );
|
|
}
|
|
|
|
return empty( $lines ) ? '' : "RELEVANT RESEARCH\n" . implode( "\n", $lines );
|
|
}
|
|
|
|
/**
|
|
* Count included research items.
|
|
*
|
|
* @param array $session_context Session context.
|
|
* @param array $request_params Request params.
|
|
* @param int $limit Max snippets.
|
|
* @return int Count.
|
|
*/
|
|
private function count_research_items( $session_context, $request_params, $limit ) {
|
|
if ( $limit <= 0 ) {
|
|
return 0;
|
|
}
|
|
|
|
$count = 0;
|
|
if ( ! empty( $session_context['research_notes'] ) && is_array( $session_context['research_notes'] ) ) {
|
|
$count += count( $session_context['research_notes'] );
|
|
}
|
|
if ( ! empty( $request_params['researchNotes'] ) && is_array( $request_params['researchNotes'] ) ) {
|
|
$count += count( $request_params['researchNotes'] );
|
|
}
|
|
|
|
return min( $limit, $count );
|
|
}
|
|
|
|
/**
|
|
* Truncate text safely.
|
|
*
|
|
* @param string $text Text.
|
|
* @param int $limit Character limit.
|
|
* @return string Truncated text.
|
|
*/
|
|
private function truncate_text( $text, $limit ) {
|
|
$text = trim( (string) $text );
|
|
if ( strlen( $text ) <= $limit ) {
|
|
return $text;
|
|
}
|
|
|
|
return substr( $text, 0, $limit ) . '...';
|
|
}
|
|
|
|
/**
|
|
* Estimate tokens from character length.
|
|
*
|
|
* @param string $text Text.
|
|
* @return int Estimated tokens.
|
|
*/
|
|
private function estimate_tokens( $text ) {
|
|
return (int) ceil( strlen( (string) $text ) / 4 );
|
|
}
|
|
}
|