Viewing File: /home/rtsgrob/ww5/wp-content/plugins/learnpress/inc/AI/Assistant/IntentClassifier.php
<?php
namespace LearnPress\AI\Assistant;
use LearnPress\Services\OpenAiService;
/**
* IntentClassifier — classifies learner intent via OpenAI.
*
* Accepts raw learner input + conversation history and returns one of the
* supported intent slugs (summarize, explain, quick_quiz, smart_review, general).
*
* @package LearnPress\AI\Assistant
* @since 4.3.5
*/
class IntentClassifier {
public const INTENT_SUMMARIZE = 'summarize';
public const INTENT_EXPLAIN = 'explain';
public const INTENT_QUICK_QUIZ = 'quick_quiz';
public const INTENT_SMART_REVIEW = 'smart_review';
public const INTENT_GENERAL = 'general';
private const HISTORY_LIMIT = 3;
private const HISTORY_CONTENT_MAX_CHARS = 300;
private TokenQuotaGuard $quota_guard;
private ResponseNormalizer $normalizer;
public function __construct( TokenQuotaGuard $quota_guard, ResponseNormalizer $normalizer ) {
$this->quota_guard = $quota_guard;
$this->normalizer = $normalizer;
}
/**
* Classify learner intent.
*
* Returns self::INTENT_GENERAL when classifier is inconclusive or on error.
*
* @param string $message Learner input.
* @param array $history Conversation history.
* @param int $item_id Current item ID (lesson or quiz).
* @param int $course_id Current course ID.
* @param int $user_id Current user ID.
*
* @return string One of the INTENT_* constants.
*/
public function classify( string $message, array $history, int $item_id, int $course_id, int $user_id ): string {
$detected = $this->classify_with_openai( $message, $history, $item_id, $course_id, $user_id );
if ( $this->is_supported_intent( $detected ) ) {
return $detected;
}
return self::INTENT_GENERAL;
}
/**
* Return whether intent is one of the known values.
*
* @param string $intent Intent slug.
*
* @return bool
*/
public function is_supported_intent( string $intent ): bool {
return in_array(
$intent,
array(
self::INTENT_SUMMARIZE,
self::INTENT_EXPLAIN,
self::INTENT_QUICK_QUIZ,
self::INTENT_SMART_REVIEW,
self::INTENT_GENERAL,
),
true
);
}
// ----------------------------------------------------------------
// Private helpers
// ----------------------------------------------------------------
/**
* Ask OpenAI to classify learner intent from natural language.
*
* @param string $message Learner input.
* @param array $history Conversation history.
* @param int $item_id Current item ID.
* @param int $course_id Current course ID.
* @param int $user_id Current user ID.
*
* @return string
*/
private function classify_with_openai( string $message, array $history, int $item_id, int $course_id, int $user_id ): string {
$service = OpenAiService::instance();
$item_type = (string) get_post_type( $item_id );
$conversation_slice = $this->slice_recent_history( $history, self::HISTORY_LIMIT );
$context_payload = array(
'item_id' => $item_id,
'course_id' => $course_id,
'item_type' => $item_type,
);
$messages = array(
array(
'role' => 'system',
'content' => __( 'You classify learner intent for LearnPress AI Assistant.', 'learnpress' ),
),
array(
'role' => 'system',
'content' => __( 'Return ONLY valid JSON object in this exact shape: {"intent":"<value>"}. Allowed values: summarize, explain, quick_quiz, smart_review, general. Do not add extra keys.', 'learnpress' ),
),
array(
'role' => 'system',
'content' => __( 'Rules: use smart_review only when the current item type is quiz and the learner asks for quiz-result feedback. If uncertain, return general.', 'learnpress' ),
),
array(
'role' => 'system',
'content' => sprintf(
/* translators: %s: JSON context payload for intent classification. */
__( 'Intent context (JSON): %s', 'learnpress' ),
wp_json_encode( $context_payload )
),
),
);
foreach ( $conversation_slice as $item ) {
$messages[] = array(
'role' => $item['role'],
'content' => $item['content'],
);
}
$messages[] = array(
'role' => 'user',
'content' => $message,
);
try {
$response = $this->quota_guard->send_chat_with_guard( $service, $messages, $user_id );
} catch ( \Throwable $e ) {
return '';
}
return $this->parse_intent_from_response( (string) ( $response['content'] ?? '' ) );
}
/**
* Parse classifier JSON response into a supported intent string.
*
* @param string $content Raw OpenAI content.
*
* @return string
*/
private function parse_intent_from_response( string $content ): string {
$content = trim( $content );
if ( $content === '' ) {
return '';
}
$decoded = $this->normalizer->decode_json( $content );
if ( ! empty( $decoded ) ) {
$intent_raw = '';
foreach ( array( 'intent', 'action', 'type' ) as $key ) {
if ( isset( $decoded[ $key ] ) && is_string( $decoded[ $key ] ) ) {
$intent_raw = $decoded[ $key ];
break;
}
}
if ( $intent_raw !== '' ) {
return $this->normalize_intent_value( $intent_raw );
}
}
if ( preg_match( '/\b(summarize|summary|explain|quick[\s_-]*quiz|quickquiz|smart[\s_-]*review|general)\b/ui', $content, $matches ) ) {
return $this->normalize_intent_value( (string) $matches[1] );
}
return '';
}
/**
* Normalize classifier intent value to one of supported constants.
*
* @param string $intent Raw intent value from classifier.
*
* @return string
*/
private function normalize_intent_value( string $intent ): string {
$normalized = strtolower( trim( $intent ) );
$normalized = preg_replace( '/\s+/', '_', $normalized ) ?? $normalized;
$normalized = str_replace( '-', '_', $normalized );
$aliases = array(
'quickquiz' => self::INTENT_QUICK_QUIZ,
'quick_quiz' => self::INTENT_QUICK_QUIZ,
'quiz' => self::INTENT_QUICK_QUIZ,
'review' => self::INTENT_SMART_REVIEW,
'summary' => self::INTENT_SUMMARIZE,
'smartreview' => self::INTENT_SMART_REVIEW,
'smart_review' => self::INTENT_SMART_REVIEW,
);
if ( isset( $aliases[ $normalized ] ) ) {
$normalized = $aliases[ $normalized ];
}
return $this->is_supported_intent( $normalized ) ? $normalized : '';
}
/**
* Keep only recent and valid chat history rows.
*
* @param array $history Chat history.
* @param int $limit Number of rows to keep from the end.
*
* @return array<int, array{role: string, content: string}>
*/
private function slice_recent_history( array $history, int $limit ): array {
$sanitized = array();
foreach ( $history as $item ) {
if ( ! is_array( $item ) ) {
continue;
}
$role = (string) ( $item['role'] ?? '' );
$content = trim( (string) ( $item['content'] ?? '' ) );
if ( ! in_array( $role, array( 'user', 'assistant' ), true ) || $content === '' ) {
continue;
}
$sanitized[] = array(
'role' => $role,
'content' => mb_substr( $content, 0, self::HISTORY_CONTENT_MAX_CHARS, 'UTF-8' ),
);
}
if ( empty( $sanitized ) ) {
return array();
}
return array_slice( $sanitized, -1 * max( 1, $limit ) );
}
}
Back to Directory
File Manager