PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.3.9
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.3.9
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 / AIAssistantController.php

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

216 lines 5.8 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 Exception;
6 use LP_Settings;
7 use LearnPress\Services\OpenAiService;
8
9 /**
10 * AI Assistant Controller — validates requests, sanitizes input, calls Agent.
11 *
12 * Entry point for the AJAX layer. Guarantees the response structure
13 * required by the frontend: { type, message, quiz }.
14 *
15 * @package LearnPress\AI\Assistant
16 * @since 4.3.5
17 */
18 class AIAssistantController {
19 private const ACTION_SETTINGS = array(
20 'summarize' => 'ai_assistant_summarize_enabled',
21 'explain' => 'ai_assistant_explain_enabled',
22 'quick_quiz' => 'ai_assistant_quick_quiz_enabled',
23 'smart_review' => 'ai_assistant_smart_review_enabled',
24 );
25
26 /**
27 * Check if the AI Assistant feature is fully enabled.
28 *
29 * All three gates must pass:
30 * - enable_open_ai = yes
31 * - secret key exists
32 * - ai_assistant_enabled = yes
33 *
34 * @return bool
35 */
36 public static function is_enabled(): bool {
37 $service = OpenAiService::instance();
38
39 if ( ! $service->is_enable() ) {
40 return false;
41 }
42
43 if ( empty( LP_Settings::get_option( 'open_ai_secret_key', '' ) ) ) {
44 return false;
45 }
46
47 return LP_Settings::get_option( 'ai_assistant_enabled', 'no' ) === 'yes';
48 }
49
50 /**
51 * Resolve per-action enabled flags from admin settings.
52 *
53 * @return array<string, bool>
54 */
55 public static function get_enabled_actions(): array {
56 $enabled_actions = array();
57
58 foreach ( self::ACTION_SETTINGS as $action => $setting_key ) {
59 $enabled_actions[ $action ] = LP_Settings::get_option( $setting_key, 'yes' ) === 'yes';
60 }
61
62 return $enabled_actions;
63 }
64
65 /**
66 * Check whether a specific assistant action is enabled.
67 *
68 * @param string $action Action slug.
69 *
70 * @return bool
71 */
72 public static function is_action_enabled( string $action ): bool {
73 $enabled_actions = self::get_enabled_actions();
74
75 return $enabled_actions[ $action ] ?? true;
76 }
77
78 /**
79 * Handle an assistant chat request.
80 *
81 * @param array $data Raw decoded data from the AJAX request.
82 *
83 * @return array{type: string, message: string, quiz: array|null}
84 * @throws Exception On validation failure or API error.
85 */
86 public function handle_chat( array $data ): array {
87 $message = trim( $data['message'] ?? '' );
88 $item_id = absint( $data['item_id'] ?? 0 );
89 $course_id = absint( $data['course_id'] ?? 0 );
90 $history = $data['history'] ?? array();
91 $quiz_data = $data['active_quiz_questions'] ?? array();
92 $action_hint = $this->sanitize_action_hint( $data['action_hint'] ?? '' );
93
94 if ( $message === '' ) {
95 throw new Exception( __( 'Message is required.', 'learnpress' ) );
96 }
97
98 if ( empty( $item_id ) ) {
99 throw new Exception( __( 'Item ID is required.', 'learnpress' ) );
100 }
101
102 if ( $course_id === 0 ) {
103 throw new Exception( __( 'Course ID is required.', 'learnpress' ) );
104 }
105
106 $user_id = get_current_user_id();
107
108 if ( $user_id === 0 ) {
109 throw new Exception( __( 'User must be logged in.', 'learnpress' ) );
110 }
111
112 // Sanitize history — only allow safe role/content pairs.
113 $sanitized_history = array();
114 if ( is_array( $history ) ) {
115 foreach ( $history as $msg ) {
116 $role = $msg['role'] ?? '';
117 $content = $msg['content'] ?? '';
118
119 if ( in_array( $role, array( 'user', 'assistant' ), true ) && is_string( $content ) ) {
120 $sanitized_history[] = array(
121 'role' => $role,
122 'content' => sanitize_textarea_field( $content ),
123 );
124 }
125 }
126 }
127
128 $agent = new Agent();
129 $sanitized_quiz_state = $this->sanitize_active_quiz_state( $quiz_data );
130
131 return $agent->run(
132 sanitize_textarea_field( $message ),
133 $item_id,
134 $course_id,
135 $user_id,
136 $sanitized_history,
137 $sanitized_quiz_state,
138 $action_hint
139 );
140 }
141
142 /**
143 * Sanitize optional quick-action hint from frontend.
144 *
145 * @param mixed $action_hint Raw action hint.
146 *
147 * @return string|null
148 */
149 private function sanitize_action_hint( $action_hint ): ?string {
150 if ( ! is_scalar( $action_hint ) ) {
151 return null;
152 }
153
154 $normalized = strtolower( trim( (string) $action_hint ) );
155 if ( '' === $normalized ) {
156 return null;
157 }
158
159 $normalized = str_replace( '-', '_', $normalized );
160
161 $aliases = array(
162 'quick_quiz' => 'quick_quiz',
163 'summarize' => 'summarize',
164 'explain' => 'explain',
165 'smart_review' => 'smart_review',
166 );
167
168 return $aliases[ $normalized ] ?? null;
169 }
170
171 /**
172 * Sanitize active quick-quiz state from frontend.
173 *
174 * @param mixed $quiz_data
175 *
176 * @return array
177 */
178 private function sanitize_active_quiz_state( $quiz_data ): array {
179 if ( ! is_array( $quiz_data ) ) {
180 return array();
181 }
182
183 $questions = array();
184 if ( ! empty( $quiz_data['questions'] ) && is_array( $quiz_data['questions'] ) ) {
185 foreach ( $quiz_data['questions'] as $question ) {
186 if ( ! is_array( $question ) ) {
187 continue;
188 }
189
190 $options = array();
191 if ( ! empty( $question['options'] ) && is_array( $question['options'] ) ) {
192 foreach ( $question['options'] as $option ) {
193 $options[] = sanitize_text_field( (string) $option );
194 }
195 }
196
197 $questions[] = array(
198 'question' => sanitize_text_field( (string) ( $question['question'] ?? '' ) ),
199 'options' => $options,
200 'correct_index' => absint( $question['correct_index'] ?? 0 ),
201 'explanation' => sanitize_textarea_field( (string) ( $question['explanation'] ?? '' ) ),
202 );
203 }
204 }
205
206 return array(
207 'is_active' => ! empty( $quiz_data['is_active'] ),
208 'completed' => ! empty( $quiz_data['completed'] ),
209 'current_index' => absint( $quiz_data['current_index'] ?? 0 ),
210 'score' => absint( $quiz_data['score'] ?? 0 ),
211 'total' => absint( $quiz_data['total'] ?? count( $questions ) ),
212 'questions' => $questions,
213 );
214 }
215 }
216