false,
'message' => __( 'Please enter a question.', 'ait-ai-admin-assistance' ),
);
}
// Allow custom integration via filter (kept for compatibility with your ecosystem).
$payload = array(
'question' => $question,
'context' => $context,
);
$filtered = apply_filters( 'ait_aiaa_ai_response', null, $payload );
if ( is_array( $filtered ) && isset( $filtered['ok'] ) ) {
return $filtered;
}
// Native OpenAI integration (Free) once settings are provided.
$api_key = trim( (string) AIT_AIAA_Settings::get( 'openai_api_key' ) );
if ( '' === $api_key ) {
return array(
'ok' => true,
'answer_html' => '
' . esc_html__( 'OpenAI is not configured yet.', 'ait-ai-admin-assistance' ) . '
' . esc_html__( 'Enter your OpenAI API key in Admin Guidance → Settings → AI Assistant.', 'ait-ai-admin-assistance' ) . '
',
'meta' => array(),
);
}
$model = trim( (string) AIT_AIAA_Settings::get( 'openai_model' ) );
if ( '' === $model ) { $model = 'gpt-4.1-mini'; }
$temperature = (float) AIT_AIAA_Settings::get( 'openai_temperature' );
$max_out = (int) AIT_AIAA_Settings::get( 'openai_max_output_tokens' );
$system = $this->build_system_prompt( $context );
$user = $this->build_user_prompt( $question, $context );
$input = array(
array( 'role' => 'system', 'content' => $system ),
);
// Pro: optional conversation transcript messages (added only when user opts in).
if ( ! empty( $context['conversation_messages'] ) && is_array( $context['conversation_messages'] ) ) {
foreach ( $context['conversation_messages'] as $m ) {
if ( ! is_array( $m ) ) { continue; }
$role = isset( $m['role'] ) ? (string) $m['role'] : '';
$content = isset( $m['content'] ) ? (string) $m['content'] : '';
$role = in_array( $role, array( 'user', 'assistant' ), true ) ? $role : '';
$content = trim( $content );
if ( '' === $role || '' === $content ) { continue; }
$input[] = array( 'role' => $role, 'content' => $content );
}
}
// Current question (with screen context)
$input[] = array( 'role' => 'user', 'content' => $user );
$body = array(
'model' => $model,
'input' => $input,
'temperature' => $temperature,
);
if ( ! empty( $max_out ) ) { $body['max_output_tokens'] = intval( $max_out ); }
$start = microtime( true );
AIT_AIAA_Logger::log( 'openai_request', array(
'model' => $model,
'temperature' => $temperature,
'max_out' => ! empty( $max_out ) ? intval( $max_out ) : 0,
'language' => $language,
'input_count' => is_array( $input ) ? count( $input ) : 0,
) );
$response = wp_remote_post( 'https://api.openai.com/v1/responses', array(
'timeout' => 30,
'headers' => array(
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( $body ),
) );
if ( is_wp_error( $response ) ) {
AIT_AIAA_Logger::log( 'openai_wp_error', array(
'error' => $response->get_error_message(),
'elapsed_ms' => (int) round( ( microtime( true ) - $start ) * 1000 ),
) );
return array(
'ok' => false,
'message' => $response->get_error_message(),
);
}
$code = (int) wp_remote_retrieve_response_code( $response );
$elapsed_ms = (int) round( ( microtime( true ) - $start ) * 1000 );
AIT_AIAA_Logger::log( 'openai_response', array(
'http_code' => $code,
'elapsed_ms' => $elapsed_ms,
) );
$raw = (string) wp_remote_retrieve_body( $response );
$data = json_decode( $raw, true );
if ( $code < 200 || $code >= 300 || ! is_array( $data ) ) {
AIT_AIAA_Logger::log( 'openai_http_error', array(
'http_code' => $code,
'body' => $raw,
) );
$msg = __( 'OpenAI request failed.', 'ait-ai-admin-assistance' );
if ( is_array( $data ) && isset( $data['error']['message'] ) ) {
$msg = sanitize_text_field( $data['error']['message'] );
}
return array( 'ok' => false, 'message' => $msg );
}
$text = $this->extract_text_from_responses_api( $data );
if ( '' === $text ) {
return array( 'ok' => false, 'message' => __( 'No answer returned.', 'ait-ai-admin-assistance' ) );
}
// Basic formatting.
$answer_html = $this->format_answer_text_to_html( $text );
return array(
'ok' => true,
'answer_html' => $answer_html,
'meta' => array(
'model' => $model,
),
);
}
protected function extract_text_from_responses_api( $data ) {
// Responses API usually returns output[] with content[] blocks.
if ( isset( $data['output'] ) && is_array( $data['output'] ) ) {
$out = '';
foreach ( $data['output'] as $item ) {
if ( empty( $item['content'] ) || ! is_array( $item['content'] ) ) { continue; }
foreach ( $item['content'] as $c ) {
if ( isset( $c['type'] ) && 'output_text' === $c['type'] && isset( $c['text'] ) ) {
$out .= (string) $c['text'];
} elseif ( isset( $c['type'] ) && 'text' === $c['type'] && isset( $c['text'] ) ) {
$out .= (string) $c['text'];
}
}
}
return trim( $out );
}
// Fallbacks.
if ( isset( $data['output_text'] ) ) {
return trim( (string) $data['output_text'] );
}
return '';
}
/**
* Convert lightweight markdown-ish output into safe HTML.
* Special rule: numbered bold headings like "1. **Title**" become Title
.
*/
protected function format_answer_text_to_html( $text ) {
$text = (string) $text;
$text = str_replace( array( "\r\n", "\r" ), "\n", $text );
$lines = explode( "\n", $text );
$out = array();
foreach ( $lines as $line ) {
$raw = trim( $line );
if ( '' === $raw ) {
$out[] = '';
continue;
}
// 1. **Heading** -> Heading
if ( preg_match( '/^\d+\.\s+\*\*(.+?)\*\*\s*$/', $raw, $mm ) ) {
$title = trim( $mm[1] );
if ( '' !== $title ) {
$out[] = '' . esc_html( $title ) . '
';
continue;
}
}
// Inline conversions: **bold**, *italic*, `code`
$html = $raw;
// Code first (avoid interfering with asterisks inside code).
$html = preg_replace_callback( '/`([^`]+)`/', function( $m ) {
return '' . esc_html( $m[1] ) . '';
}, $html );
// Bold
$html = preg_replace_callback( '/\*\*([^*]+)\*\*/', function( $m ) {
return '' . esc_html( $m[1] ) . '';
}, $html );
// Italic (single asterisks), avoid matching inside tags.
$html = preg_replace_callback( '/(^|\s)\*([^*]+)\*(\s|$)/', function( $m ) {
return $m[1] . '' . esc_html( $m[2] ) . '' . $m[3];
}, $html );
// Escape any remaining raw HTML.
// At this point, only our inserted tags should remain.
$out[] = wp_kses_post( $html );
}
$joined = implode( "\n", $out );
// wpautop will wrap plain lines into and keep block tags like
intact.
return wp_kses_post( wpautop( $joined ) );
}
protected function build_system_prompt( $context ) {
$role = isset( $context['user_role'] ) ? sanitize_text_field( $context['user_role'] ) : '';
$can_manage = ! empty( $context['can_manage_options'] );
$can_edit_posts = ! empty( $context['can_edit_posts'] );
$style = (string) AIT_AIAA_Settings::get( 'ai_response_style' );
$language = (string) AIT_AIAA_Settings::get( 'openai_default_language' );
$language = $language ? $language : 'auto';
$lines = array();
$lines[] = 'You are an admin assistant inside the WordPress dashboard.';
$lines[] = 'Answer the user question about the current admin screen and how to complete tasks safely.';
$lines[] = 'Be concise and step-by-step where helpful.';
$lines[] = 'Do not invent plugin features or settings. If unsure, say what to check in the UI.';
$lines[] = 'Avoid recommending direct code changes unless explicitly requested.';
if ( $language !== 'auto' ) {
$opts = AIT_AIAA_Settings::get_language_options();
$lang_name = isset( $opts[ $language ] ) ? $opts[ $language ] : $language;
$lines[] = 'Respond in ' . $lang_name . '.';
$lines[] = 'Keep WordPress UI labels in English when referring to menu items or screen labels that appear in the dashboard.';
}
// Role shaping.
if ( $role ) {
$lines[] = 'The current user role is: ' . $role . '.';
}
if ( ! $can_manage ) {
$lines[] = 'Assume the user may NOT have administrator access (cannot manage options). Prefer editor-safe steps and mention when an action likely requires an admin.';
}
if ( ! $can_edit_posts ) {
$lines[] = 'Assume the user may have limited editing permissions. Offer general navigation help and ask them to contact an admin if they cannot access a menu.';
}
if ( 'concise' === $style ) {
$lines[] = 'Response style: concise.';
} elseif ( 'detailed' === $style ) {
$lines[] = 'Response style: detailed.';
} else {
$lines[] = 'Response style: balanced.';
}
return implode( "\n", $lines );
}
protected function build_user_prompt( $question, $context ) {
$screen_id = isset( $context['screen_id'] ) ? sanitize_text_field( $context['screen_id'] ) : '';
$post_type = isset( $context['post_type'] ) ? sanitize_text_field( $context['post_type'] ) : '';
$taxonomy = isset( $context['taxonomy'] ) ? sanitize_text_field( $context['taxonomy'] ) : '';
$admin_path = isset( $context['admin_path'] ) ? sanitize_text_field( $context['admin_path'] ) : '';
$lines = array();
$lines[] = 'Current screen_id: ' . $screen_id;
$lines[] = 'Current post_type: ' . $post_type;
$lines[] = 'Current taxonomy: ' . $taxonomy;
if ( $admin_path ) { $lines[] = 'Current admin path: ' . $admin_path; }
// Designer guidance (screen + role).
if ( ! empty( $context['guidance_text'] ) ) {
$lines[] = '--- Designer guidance ---';
$lines[] = (string) $context['guidance_text'];
}
// Extra context (headings + field schema), only when enabled.
if ( ! empty( $context['visible_headings'] ) && is_array( $context['visible_headings'] ) ) {
$vh = $context['visible_headings'];
$lines[] = '--- Visible headings & metabox titles ---';
if ( ! empty( $vh['h1'] ) ) { $lines[] = 'H1: ' . sanitize_text_field( $vh['h1'] ); }
if ( ! empty( $vh['h2'] ) && is_array( $vh['h2'] ) ) { $lines[] = 'H2: ' . implode( ' | ', array_map( 'sanitize_text_field', $vh['h2'] ) ); }
if ( ! empty( $vh['h3'] ) && is_array( $vh['h3'] ) ) { $lines[] = 'H3: ' . implode( ' | ', array_map( 'sanitize_text_field', $vh['h3'] ) ); }
if ( ! empty( $vh['metabox_titles'] ) && is_array( $vh['metabox_titles'] ) ) { $lines[] = 'Metaboxes: ' . implode( ' | ', array_map( 'sanitize_text_field', $vh['metabox_titles'] ) ); }
}
if ( ! empty( $context['field_schema'] ) && is_array( $context['field_schema'] ) ) {
$lines[] = '--- Form field schema (no values) ---';
$count = 0;
foreach ( $context['field_schema'] as $row ) {
if ( ! is_array( $row ) ) { continue; }
$label = isset( $row['label'] ) ? sanitize_text_field( $row['label'] ) : '';
$type = isset( $row['type'] ) ? sanitize_text_field( $row['type'] ) : '';
$name = isset( $row['name'] ) ? sanitize_text_field( $row['name'] ) : '';
$lines[] = '- ' . ( $label ? $label : '(unlabeled)' ) . ' [type=' . $type . ' name=' . $name . ']';
$count++;
if ( $count >= 80 ) { break; }
}
}
// Optional site context.
if ( AIT_AIAA_Settings::get( 'ai_include_site_context' ) ) {
$lines[] = '--- Site context ---';
$lines[] = $this->get_site_context_summary();
}
$lines[] = '--- Question ---';
$lines[] = $question;
return implode( "\n", $lines );
}
protected function get_site_context_summary() {
$parts = array();
// Active theme.
$theme = function_exists( 'wp_get_theme' ) ? wp_get_theme() : null;
if ( $theme ) {
$parts[] = 'Active theme: ' . sanitize_text_field( $theme->get( 'Name' ) ) . ' (' . sanitize_text_field( $theme->get_stylesheet() ) . ')';
}
// Active plugins (names only).
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugins = get_plugins();
$active = (array) get_option( 'active_plugins', array() );
$active_names = array();
foreach ( $active as $file ) {
if ( isset( $plugins[ $file ]['Name'] ) ) {
$active_names[] = sanitize_text_field( $plugins[ $file ]['Name'] );
}
}
if ( ! empty( $active_names ) ) {
$parts[] = 'Active plugins: ' . implode( ', ', $active_names );
}
// Custom post types summary (public non-built-in).
$cpts = get_post_types( array( '_builtin' => false ), 'objects' );
if ( ! empty( $cpts ) ) {
$summaries = array();
foreach ( $cpts as $pt ) {
$label = isset( $pt->labels->singular_name ) ? $pt->labels->singular_name : $pt->name;
$summaries[] = $label . ' (key: ' . $pt->name . ', public: ' . ( $pt->public ? 'yes' : 'no' ) . ', has_archive: ' . ( $pt->has_archive ? 'yes' : 'no' ) . ')';
if ( count( $summaries ) >= 10 ) { break; }
}
$parts[] = 'Custom post types: ' . implode( ' | ', $summaries );
}
return implode( "\n", $parts );
}
}