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 / ResponseNormalizer.php

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

250 lines 5.5 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 /**
6 * ResponseNormalizer — decodes JSON model output and extracts human-readable text.
7 *
8 * Handles the common pattern where OpenAI returns JSON-wrapped text such as
9 * {"message":"..."} and extracts the best text candidate for frontend rendering.
10 *
11 * @package LearnPress\AI\Assistant
12 * @since 4.3.5
13 */
14 class ResponseNormalizer {
15
16 /**
17 * Decode JSON content safely without throwing exceptions.
18 *
19 * Accepts either pure JSON or content containing a JSON object substring.
20 * Returns empty array when content is plain text or invalid JSON.
21 *
22 * @param string $content Raw model content.
23 *
24 * @return array
25 */
26 public function decode_json( string $content ): array {
27 $content = trim( $content );
28 if ( '' === $content ) {
29 return array();
30 }
31
32 $decoded = json_decode( $content, true );
33 if ( is_array( $decoded ) && JSON_ERROR_NONE === json_last_error() ) {
34 return $decoded;
35 }
36
37 $first_brace = strpos( $content, '{' );
38 $last_brace = strrpos( $content, '}' );
39 if ( false === $first_brace || false === $last_brace || $last_brace <= $first_brace ) {
40 return array();
41 }
42
43 $json_slice = substr( $content, $first_brace, $last_brace - $first_brace + 1 );
44 if ( ! is_string( $json_slice ) || '' === $json_slice ) {
45 return array();
46 }
47
48 $decoded = json_decode( $json_slice, true );
49
50 return ( is_array( $decoded ) && JSON_ERROR_NONE === json_last_error() ) ? $decoded : array();
51 }
52
53 /**
54 * Convert model JSON output into readable assistant text.
55 *
56 * @param string $content Raw model content.
57 *
58 * @return string
59 */
60 public function normalize( string $content ): string {
61 $content = trim( $content );
62 if ( '' === $content ) {
63 return '';
64 }
65
66 $decoded = $this->decode_json( $content );
67 if ( empty( $decoded ) ) {
68 if ( $this->looks_like_metadata_value( $content ) ) {
69 return __( 'I could not generate a complete answer. Please try rephrasing your request.', 'learnpress' );
70 }
71
72 return $content;
73 }
74
75 $extracted = $this->extract_text_from_array( $decoded );
76
77 if ( '' !== $extracted ) {
78 return $extracted;
79 }
80
81 return $this->looks_like_metadata_value( $content )
82 ? __( 'I could not generate a complete answer. Please try rephrasing your request.', 'learnpress' )
83 : $content;
84 }
85
86 /**
87 * Build normalized response from assistant content.
88 *
89 * @param string $content The raw assistant text content.
90 *
91 * @return array{type: string, message: string, quiz: array|null}
92 */
93 public function build_response( string $content ): array {
94 return array(
95 'type' => 'text',
96 'message' => $this->normalize( $content ),
97 'quiz' => null,
98 );
99 }
100
101 /**
102 * Extract first meaningful text value from a decoded JSON object.
103 *
104 * @param array $data Decoded JSON.
105 *
106 * @return string
107 */
108 private function extract_text_from_array( array $data ): string {
109 $preferred_keys = array(
110 'message',
111 'answer',
112 'content',
113 'summary',
114 'explanation',
115 'text',
116 'response',
117 'result',
118 'key_points',
119 'main_points',
120 'bullet_points',
121 );
122
123 foreach ( $preferred_keys as $key ) {
124 if ( empty( $data[ $key ] ) ) {
125 continue;
126 }
127
128 $value = $data[ $key ];
129 if ( is_string( $value ) && '' !== trim( $value ) ) {
130 return trim( $value );
131 }
132
133 if ( is_array( $value ) ) {
134 $nested = $this->extract_text_from_array( $value );
135 if ( '' !== $nested ) {
136 return $nested;
137 }
138 }
139 }
140
141 foreach ( $data as $key => $value ) {
142 if ( $this->is_metadata_text_key( (string) $key ) ) {
143 continue;
144 }
145
146 if ( is_array( $value ) ) {
147 $nested = $this->extract_text_from_array( $value );
148 if ( '' !== $nested ) {
149 return $nested;
150 }
151 }
152 }
153
154 foreach ( $data as $key => $value ) {
155 if ( $this->is_metadata_text_key( (string) $key ) || ! is_string( $value ) ) {
156 continue;
157 }
158
159 $candidate = trim( $value );
160 if ( '' === $candidate || $this->looks_like_metadata_value( $candidate ) ) {
161 continue;
162 }
163
164 if ( $this->is_substantial_text( $candidate ) ) {
165 return $candidate;
166 }
167 }
168
169 return '';
170 }
171
172 /**
173 * Determine whether a decoded JSON key is metadata and should not be shown as answer text.
174 *
175 * @param string $key JSON key.
176 *
177 * @return bool
178 */
179 private function is_metadata_text_key( string $key ): bool {
180
181 $normalized = strtolower( trim( $key ) );
182 if ( $normalized === '' ) {
183 return false;
184 }
185
186 return in_array(
187 $normalized,
188 array(
189 'language',
190 'locale',
191 'intent',
192 'action',
193 'type',
194 'status',
195 'model',
196 'provider',
197 'metadata',
198 ),
199 true
200 );
201 }
202
203 /**
204 * Check whether a text candidate looks like metadata output.
205 *
206 * @param string $value Text candidate.
207 *
208 * @return bool
209 */
210 private function looks_like_metadata_value( string $value ): bool {
211
212 $trimmed = trim( $value );
213 if ( $trimmed === '' ) {
214 return true;
215 }
216
217 if ( preg_match( '/^(language|locale|intent|action|type|status)\s*[:=]\s*[\w\-]+$/ui', $trimmed ) ) {
218 return true;
219 }
220
221 return preg_match( '/^[a-z]{2}[-_][a-z]{2}$/i', $trimmed ) === 1;
222 }
223
224 /**
225 * Determine whether candidate text is substantial enough for user-facing response.
226 *
227 * @param string $value Text candidate.
228 *
229 * @return bool
230 */
231 private function is_substantial_text( string $value ): bool {
232
233 $trimmed = trim( $value );
234 if ( $trimmed === '' ) {
235 return false;
236 }
237
238 if ( preg_match( '/[.!?。!?]/u', $trimmed ) ) {
239 return true;
240 }
241
242 $length = mb_strlen( $trimmed, 'UTF-8' );
243 if ( $length >= 20 ) {
244 return true;
245 }
246
247 return $length >= 14 && preg_match( '/\s/u', $trimmed ) === 1;
248 }
249 }
250