PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.7
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.7
4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 4.2.1 All 138 releases
learnpress / inc / AI / Assistant / IntentClassifier.php

IntentClassifier.php in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.4.7, at inc/AI/Assistant/IntentClassifier.php

252 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace LearnPress\AI\Assistant;
4
5 use LearnPress\Services\OpenAiService;
6
7 /**
8 * IntentClassifier — classifies learner intent via OpenAI.
9 *
10 * Accepts raw learner input + conversation history and returns one of the
11 * supported intent slugs (summarize, explain, quick_quiz, smart_review, general).
12 *
13 * @package LearnPress\AI\Assistant
14 * @since 4.3.5
15 */
16 class IntentClassifier {
17
18 public const INTENT_SUMMARIZE = 'summarize';
19 public const INTENT_EXPLAIN = 'explain';
20 public const INTENT_QUICK_QUIZ = 'quick_quiz';
21 public const INTENT_SMART_REVIEW = 'smart_review';
22 public const INTENT_GENERAL = 'general';
23 private const HISTORY_LIMIT = 3;
24 private const HISTORY_CONTENT_MAX_CHARS = 300;
25
26 private TokenQuotaGuard $quota_guard;
27 private ResponseNormalizer $normalizer;
28
29 public function __construct( TokenQuotaGuard $quota_guard, ResponseNormalizer $normalizer ) {
30 $this->quota_guard = $quota_guard;
31 $this->normalizer = $normalizer;
32 }
33
34 /**
35 * Classify learner intent.
36 *
37 * Returns self::INTENT_GENERAL when classifier is inconclusive or on error.
38 *
39 * @param string $message Learner input.
40 * @param array $history Conversation history.
41 * @param int $item_id Current item ID (lesson or quiz).
42 * @param int $course_id Current course ID.
43 * @param int $user_id Current user ID.
44 *
45 * @return string One of the INTENT_* constants.
46 */
47 public function classify( string $message, array $history, int $item_id, int $course_id, int $user_id ): string {
48
49 $detected = $this->classify_with_openai( $message, $history, $item_id, $course_id, $user_id );
50 if ( $this->is_supported_intent( $detected ) ) {
51 return $detected;
52 }
53
54 return self::INTENT_GENERAL;
55 }
56
57 /**
58 * Return whether intent is one of the known values.
59 *
60 * @param string $intent Intent slug.
61 *
62 * @return bool
63 */
64 public function is_supported_intent( string $intent ): bool {
65
66 return in_array(
67 $intent,
68 array(
69 self::INTENT_SUMMARIZE,
70 self::INTENT_EXPLAIN,
71 self::INTENT_QUICK_QUIZ,
72 self::INTENT_SMART_REVIEW,
73 self::INTENT_GENERAL,
74 ),
75 true
76 );
77 }
78
79 // ----------------------------------------------------------------
80 // Private helpers
81 // ----------------------------------------------------------------
82
83 /**
84 * Ask OpenAI to classify learner intent from natural language.
85 *
86 * @param string $message Learner input.
87 * @param array $history Conversation history.
88 * @param int $item_id Current item ID.
89 * @param int $course_id Current course ID.
90 * @param int $user_id Current user ID.
91 *
92 * @return string
93 */
94 private function classify_with_openai( string $message, array $history, int $item_id, int $course_id, int $user_id ): string {
95
96 $service = OpenAiService::instance();
97 $item_type = (string) get_post_type( $item_id );
98 $conversation_slice = $this->slice_recent_history( $history, self::HISTORY_LIMIT );
99 $context_payload = array(
100 'item_id' => $item_id,
101 'course_id' => $course_id,
102 'item_type' => $item_type,
103 );
104
105 $messages = array(
106 array(
107 'role' => 'system',
108 'content' => __( 'You classify learner intent for LearnPress AI Assistant.', 'learnpress' ),
109 ),
110 array(
111 'role' => 'system',
112 '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' ),
113 ),
114 array(
115 'role' => 'system',
116 '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' ),
117 ),
118 array(
119 'role' => 'system',
120 'content' => sprintf(
121 /* translators: %s: JSON context payload for intent classification. */
122 __( 'Intent context (JSON): %s', 'learnpress' ),
123 wp_json_encode( $context_payload )
124 ),
125 ),
126 );
127
128 foreach ( $conversation_slice as $item ) {
129 $messages[] = array(
130 'role' => $item['role'],
131 'content' => $item['content'],
132 );
133 }
134
135 $messages[] = array(
136 'role' => 'user',
137 'content' => $message,
138 );
139
140 try {
141 $response = $this->quota_guard->send_chat_with_guard( $service, $messages, $user_id );
142 } catch ( \Throwable $e ) {
143 return '';
144 }
145
146 return $this->parse_intent_from_response( (string) ( $response['content'] ?? '' ) );
147 }
148
149 /**
150 * Parse classifier JSON response into a supported intent string.
151 *
152 * @param string $content Raw OpenAI content.
153 *
154 * @return string
155 */
156 private function parse_intent_from_response( string $content ): string {
157
158 $content = trim( $content );
159 if ( $content === '' ) {
160 return '';
161 }
162
163 $decoded = $this->normalizer->decode_json( $content );
164 if ( ! empty( $decoded ) ) {
165 $intent_raw = '';
166 foreach ( array( 'intent', 'action', 'type' ) as $key ) {
167 if ( isset( $decoded[ $key ] ) && is_string( $decoded[ $key ] ) ) {
168 $intent_raw = $decoded[ $key ];
169 break;
170 }
171 }
172
173 if ( $intent_raw !== '' ) {
174 return $this->normalize_intent_value( $intent_raw );
175 }
176 }
177
178 if ( preg_match( '/\b(summarize|summary|explain|quick[\s_-]*quiz|quickquiz|smart[\s_-]*review|general)\b/ui', $content, $matches ) ) {
179 return $this->normalize_intent_value( (string) $matches[1] );
180 }
181
182 return '';
183 }
184
185 /**
186 * Normalize classifier intent value to one of supported constants.
187 *
188 * @param string $intent Raw intent value from classifier.
189 *
190 * @return string
191 */
192 private function normalize_intent_value( string $intent ): string {
193
194 $normalized = strtolower( trim( $intent ) );
195 $normalized = preg_replace( '/\s+/', '_', $normalized ) ?? $normalized;
196 $normalized = str_replace( '-', '_', $normalized );
197
198 $aliases = array(
199 'quickquiz' => self::INTENT_QUICK_QUIZ,
200 'quick_quiz' => self::INTENT_QUICK_QUIZ,
201 'quiz' => self::INTENT_QUICK_QUIZ,
202 'review' => self::INTENT_SMART_REVIEW,
203 'summary' => self::INTENT_SUMMARIZE,
204 'smartreview' => self::INTENT_SMART_REVIEW,
205 'smart_review' => self::INTENT_SMART_REVIEW,
206 );
207
208 if ( isset( $aliases[ $normalized ] ) ) {
209 $normalized = $aliases[ $normalized ];
210 }
211
212 return $this->is_supported_intent( $normalized ) ? $normalized : '';
213 }
214
215 /**
216 * Keep only recent and valid chat history rows.
217 *
218 * @param array $history Chat history.
219 * @param int $limit Number of rows to keep from the end.
220 *
221 * @return array<int, array{role: string, content: string}>
222 */
223 private function slice_recent_history( array $history, int $limit ): array {
224
225 $sanitized = array();
226
227 foreach ( $history as $item ) {
228 if ( ! is_array( $item ) ) {
229 continue;
230 }
231
232 $role = (string) ( $item['role'] ?? '' );
233 $content = trim( (string) ( $item['content'] ?? '' ) );
234
235 if ( ! in_array( $role, array( 'user', 'assistant' ), true ) || $content === '' ) {
236 continue;
237 }
238
239 $sanitized[] = array(
240 'role' => $role,
241 'content' => mb_substr( $content, 0, self::HISTORY_CONTENT_MAX_CHARS, 'UTF-8' ),
242 );
243 }
244
245 if ( empty( $sanitized ) ) {
246 return array();
247 }
248
249 return array_slice( $sanitized, -1 * max( 1, $limit ) );
250 }
251 }
252