PluginProbe ʕ •ᴥ•ʔ
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI / 3.5.2
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI v3.5.2
3.5.2 3.5.1 3.5.0 3.4.8 3.4.7 3.4.6 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5.1 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.10 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.6.1 1.6.7 1.7.0 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.2 1.7.2.1 1.7.2.2 1.7.3 1.7.4 1.7.5 1.7.5.1 1.7.5.2 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.8.0 1.8.0.1 1.8.1 1.8.2 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.0.1 1.9.1 1.9.2 1.9.3 1.9.4 1.9.4.1 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.0.1 2.0.1 2.0.2 2.0.3 2.0.3.1 2.0.4 2.0.4.1 2.0.5 2.0.6 2.0.7 2.0.8 2.0.8.1 2.0.9 3.0.0 3.0.0.1 3.0.1 3.0.2 3.0.3 3.0.3.1 3.0.4 3.0.4.1 3.0.4.2 3.0.5 3.0.5.1 3.0.5.2 3.0.6 3.0.6.1 3.0.7.1 3.0.8 3.0.8.1 3.0.9 3.0.9.1 3.0.9.2 3.0.9.3 3.0.9.4 3.0.9.5 3.1.0 3.1.1 3.1.2 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.3.0 3.4.0 3.4.1 3.4.2 3.4.2.1 3.4.3 3.4.4 3.4.5 trunk 1.0 1.0.1 1.0.2 1.0.3
everest-forms / includes / Integrations / AI / class-evf-ai-api.php
everest-forms / includes / Integrations / AI Last commit date
class-evf-ai-ajax.php 3 weeks ago class-evf-ai-api.php 3 weeks ago class-evf-ai-form-builder.php 4 weeks ago class-evf-ai-loader.php 4 weeks ago class-evf-ai-registration.php 4 weeks ago
class-evf-ai-api.php
370 lines
1 <?php
2 /**
3 * EVF AI API — HTTP client for the ThemeGrill AI Cloud gateway.
4 *
5 * Gateway URL is read from:
6 * 1. TG_AI_GATEWAY_URL constant (wp-config.php) — local dev override
7 * 2. 'evf_ai_gateway_url' option — settable from admin (future)
8 * 3. Hardcoded production URL as final fallback
9 *
10 * License pattern (follows WPForms): license key is sent inline with every
11 * generate request. The gateway verifies with wpeverest.com and caches for
12 * 1 week. No separate "activate" step needed.
13 */
14
15 defined( 'ABSPATH' ) || exit;
16
17 class EVF_AI_API {
18
19 const PRODUCTION_URL = 'https://ai.themegrill.com';
20 const PRODUCT = 'everest-forms';
21 const TIMEOUT = 90;
22
23 /**
24 * Generate a form from a plain-text prompt.
25 * Sends the EVF Pro license key inline — gateway verifies + caches (1 week).
26 *
27 * @param string $prompt
28 * @return array|WP_Error Decoded AI response on success.
29 */
30 public static function generate_form( string $prompt ) {
31 $token = EVF_AI_Registration::get_site_token();
32 if ( ! $token ) {
33 return new WP_Error( 'not_registered', __( 'AI features are not yet active on this site.', 'everest-forms' ) );
34 }
35
36 $logger = evf_get_logger();
37 $logger->info( sprintf( 'AI Form Generation started | prompt: %s', $prompt ), array( 'source' => 'evf-ai' ) );
38
39 // Send license key if EVF Pro is active — gateway verifies inline (WPForms pattern).
40 // If no license key, gateway treats site as free tier.
41 $license_key = self::get_license_key();
42
43 $response = self::request(
44 'POST',
45 '/ai/v1/generate',
46 array(
47 'prompt' => $prompt,
48 'license_key' => $license_key,
49 'available_fields' => implode( ',', evf()->form_fields->get_form_field_types() ),
50 ),
51 $token
52 );
53
54 if ( is_wp_error( $response ) ) {
55 $logger->error( sprintf( 'AI Form Generation failed | %s: %s', $response->get_error_code(), $response->get_error_message() ), array( 'source' => 'evf-ai' ) );
56 return $response;
57 }
58
59 if ( empty( $response['success'] ) || empty( $response['form'] ) ) {
60 $logger->error( 'AI Form Generation bad_response | missing success or form key', array( 'source' => 'evf-ai' ) );
61 return new WP_Error( 'bad_response', __( 'Unexpected response from AI service.', 'everest-forms' ) );
62 }
63
64 $logger->info(
65 sprintf( 'AI Form Generation succeeded | form_type: %s, fields: %d', $response['form']['form_type'] ?? 'standard', count( $response['form']['fields'] ?? array() ) ),
66 array( 'source' => 'evf-ai' )
67 );
68
69 return $response['form'];
70 }
71
72 /**
73 * Regenerate / refine an existing AI form from a follow-up prompt.
74 *
75 * NOTE: the gateway does not implement /ai/v1/update yet — this wires the call
76 * so it works the moment the Python endpoint ships. Until then it returns the
77 * gateway's error (surfaced to the user).
78 *
79 * @param string $prompt Refinement / follow-up prompt (or the original to regenerate).
80 * @param int $form_id The draft form being refined.
81 * @return array|WP_Error Decoded AI form schema on success.
82 */
83 public static function update_form( string $prompt, int $form_id = 0, string $refine_prompt = '' ) {
84 $token = EVF_AI_Registration::get_site_token();
85 if ( ! $token ) {
86 return new WP_Error( 'not_registered', __( 'AI features are not yet active on this site.', 'everest-forms' ) );
87 }
88
89 $logger = evf_get_logger();
90 $logger->info(
91 sprintf( 'AI Form Update started | form_id: %d, prompt: %s, refine_prompt: %s', $form_id, $prompt, $refine_prompt ),
92 array( 'source' => 'evf-ai' )
93 );
94
95 $body = array(
96 'prompt' => $prompt,
97 'refine_prompt' => $refine_prompt,
98 'form_id' => $form_id,
99 'license_key' => self::get_license_key(),
100 'current_form' => self::get_current_form_context( $form_id ),
101 );
102
103 $response = self::request( 'POST', '/ai/v1/update', $body, $token );
104
105 // Auto-heal stale token — same pattern as generate_form
106 if ( is_wp_error( $response ) && 'api_error' === $response->get_error_code()
107 && false !== strpos( $response->get_error_message(), 'Invalid token' ) ) {
108
109 $logger->warning( 'AI Form Update stale token — re-registering and retrying', array( 'source' => 'evf-ai' ) );
110 EVF_AI_Registration::clear_credentials();
111 EVF_AI_Registration::register();
112 $token = EVF_AI_Registration::get_site_token();
113 $response = self::request( 'POST', '/ai/v1/update', $body, $token );
114 }
115
116 if ( is_wp_error( $response ) ) {
117 $logger->error( sprintf( 'AI Form Update failed | %s: %s', $response->get_error_code(), $response->get_error_message() ), array( 'source' => 'evf-ai' ) );
118 return $response;
119 }
120
121 if ( empty( $response['success'] ) || empty( $response['form'] ) ) {
122 $logger->error( 'AI Form Update bad_response | missing success or form key', array( 'source' => 'evf-ai' ) );
123 return new WP_Error( 'bad_response', __( 'Unexpected response from AI service.', 'everest-forms' ) );
124 }
125
126 $logger->info(
127 sprintf( 'AI Form Update succeeded | form_id: %d, form_type: %s, fields: %d', $form_id, $response['form']['form_type'] ?? 'standard', count( $response['form']['fields'] ?? array() ) ),
128 array( 'source' => 'evf-ai' )
129 );
130
131 return $response['form'];
132 }
133
134 /**
135 * Extract a lightweight form context for the AI.
136 * Includes type, label, and any non-default field settings so that subsequent
137 * AI requests preserve changes made by earlier ones (e.g. label_hide, required).
138 *
139 * @param int $form_id
140 * @return array { form_title, fields: [ { type, label, ...settings } ] }
141 */
142 private static function get_current_form_context( int $form_id ): array {
143 if ( ! $form_id ) {
144 return [];
145 }
146
147 $post = get_post( $form_id );
148 if ( ! $post || 'everest_form' !== $post->post_type ) {
149 return [];
150 }
151
152 $data = evf_decode( $post->post_content );
153 $summary = [];
154
155 foreach ( ( $data['form_fields'] ?? [] ) as $field ) {
156 $type = $field['type'] ?? '';
157 if ( in_array( $type, [ 'hidden', 'html', 'divider' ], true ) ) {
158 continue;
159 }
160
161 $entry = [
162 'type' => $type,
163 'label' => $field['label'] ?? '',
164 ];
165
166 // Include non-default field settings so the AI can preserve them on
167 // subsequent requests without the user having to repeat their instructions.
168 if ( ! empty( $field['label_hide'] ) && '1' === $field['label_hide'] ) {
169 $entry['label_hide'] = true;
170 }
171 if ( ! empty( $field['required'] ) && '1' === $field['required'] ) {
172 $entry['required'] = true;
173 }
174 if ( ! empty( $field['description'] ) ) {
175 $entry['description'] = $field['description'];
176 }
177 if ( ! empty( $field['placeholder'] ) ) {
178 $entry['placeholder'] = $field['placeholder'];
179 }
180 if ( ! empty( $field['sublabel_hide'] ) && '1' === $field['sublabel_hide'] ) {
181 $entry['sublabel_hide'] = true;
182 }
183 if ( ! empty( $field['css'] ) ) {
184 $entry['css'] = $field['css'];
185 }
186
187 $summary[] = $entry;
188 }
189
190 // Detect form type so the gateway can preserve it during refine/regenerate
191 $form_type = 'standard';
192 if ( ! empty( $data['settings']['enable_multi_part'] ) && '1' === $data['settings']['enable_multi_part'] ) {
193 $form_type = 'multipart';
194 } elseif ( ! empty( $data['settings']['enable_conversational_forms'] ) && '1' === $data['settings']['enable_conversational_forms'] ) {
195 $form_type = 'conversational';
196 }
197
198 // Include multipart step titles so AI can preserve/extend them
199 $multipart_steps = [];
200 if ( 'multipart' === $form_type ) {
201 foreach ( ( $data['multi_part'] ?? [] ) as $part ) {
202 $multipart_steps[] = [
203 'title' => $part['name'] ?? '',
204 'field_count' => count( $part['fields'] ?? [] ),
205 ];
206 }
207 }
208
209 $email_conns = $data['settings']['email'] ?? [];
210 $conn1 = $email_conns['connection_1'] ?? [];
211
212 return [
213 'form_title' => $post->post_title,
214 'form_type' => $form_type,
215 'multipart_steps' => $multipart_steps,
216 'fields' => $summary,
217 // Settings context so the gateway preserves them on refine
218 'redirect_to' => $data['settings']['redirect_to'] ?? 'same',
219 'redirect_custom_page_id' => absint( $data['settings']['custom_page'] ?? 0 ),
220 'redirect_external_url' => $data['settings']['external_url'] ?? '',
221 'notification' => [
222 'from_name' => $conn1['evf_from_name'] ?? '',
223 'reply_to' => $conn1['evf_reply_to'] ?? 'auto',
224 'message' => $conn1['evf_email_message'] ?? '{all_fields}',
225 'subject' => $conn1['evf_email_subject'] ?? '',
226 ],
227 'user_confirmation' => ! empty( $email_conns['connection_2'] ) ? [
228 'from_name' => $email_conns['connection_2']['evf_from_name'] ?? '',
229 'reply_to' => $email_conns['connection_2']['evf_reply_to'] ?? 'auto',
230 ] : [],
231 ];
232 }
233
234 /**
235 * Register this site with the ThemeGrill AI Cloud gateway (free tier).
236 * Called once on plugin activation — silent, no admin action required.
237 *
238 * @param string $verify_token One-time ownership token; gateway calls back to confirm.
239 * @return array|WP_Error { site_token, tier, product }
240 */
241 public static function register_site( string $verify_token = '' ) {
242 $payload = array(
243 'domain' => self::get_domain(),
244 'admin_email' => get_bloginfo( 'admin_email' ),
245 'wp_version' => get_bloginfo( 'version' ),
246 'product' => self::PRODUCT,
247 );
248
249 if ( $verify_token ) {
250 $payload['verify_token'] = $verify_token;
251 }
252
253 return self::request( 'POST', '/ai/v1/register', $payload );
254 }
255
256 /**
257 * Get current usage stats for display in the builder UI.
258 *
259 * @return array|WP_Error
260 */
261 public static function get_usage() {
262 $token = EVF_AI_Registration::get_site_token();
263 if ( ! $token ) {
264 return new WP_Error( 'not_registered', '' );
265 }
266 return self::request( 'GET', '/ai/v1/usage', array(), $token );
267 }
268
269 // ── Core HTTP request ─────────────────────────────────────────────────────
270
271 private static function request( string $method, string $path, array $body = array(), string $token = '' ) {
272 $url = rtrim( self::gateway_url(), '/' ) . $path;
273 $headers = array( 'Content-Type' => 'application/json' );
274
275 if ( $token ) {
276 $headers['X-TG-Token'] = $token;
277 }
278
279 $args = array(
280 'method' => strtoupper( $method ),
281 'headers' => $headers,
282 'timeout' => self::TIMEOUT,
283 );
284
285 if ( ! empty( $body ) && 'GET' !== strtoupper( $method ) ) {
286 $args['body'] = wp_json_encode( $body );
287 }
288
289 // Log outgoing request (license_key redacted).
290 $log_body = $body;
291 if ( isset( $log_body['license_key'] ) ) {
292 $log_body['license_key'] = $log_body['license_key'] ? '[redacted]' : '';
293 }
294 $logger = evf_get_logger();
295 $logger->debug(
296 sprintf( "AI Request: %s %s\n%s", strtoupper( $method ), $path, wp_json_encode( $log_body, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) ),
297 array( 'source' => 'evf-ai' )
298 );
299
300 $wp_response = wp_remote_request( $url, $args );
301
302 if ( is_wp_error( $wp_response ) ) {
303 $logger->error(
304 sprintf( 'AI Request failed: %s', $wp_response->get_error_message() ),
305 array( 'source' => 'evf-ai' )
306 );
307 return new WP_Error(
308 'request_failed',
309 sprintf( __( 'Could not reach AI service: %s', 'everest-forms' ), $wp_response->get_error_message() )
310 );
311 }
312
313 $status = wp_remote_retrieve_response_code( $wp_response );
314 $body = json_decode( wp_remote_retrieve_body( $wp_response ), true );
315
316 // Log the raw response.
317 $logger->debug(
318 sprintf( "AI Response: HTTP %d\n%s", $status, wp_json_encode( $body, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) ),
319 array( 'source' => 'evf-ai' )
320 );
321
322 if ( 429 === $status ) {
323 $msg = is_array( $body ) && isset( $body['detail']['message'] )
324 ? $body['detail']['message']
325 : __( 'Request limit reached. Please try again later.', 'everest-forms' );
326 $code = is_array( $body ) && isset( $body['detail']['error'] )
327 ? $body['detail']['error']
328 : 'rate_limited';
329 return new WP_Error( $code, $msg );
330 }
331
332 if ( $status < 200 || $status >= 300 ) {
333 $detail = is_array( $body ) ? ( $body['detail'] ?? $body['message'] ?? '' ) : '';
334 // FastAPI 400s send detail as an object: {"error": "...", "message": "..."}.
335 $msg = is_array( $detail ) ? ( $detail['message'] ?? $detail['error'] ?? '' ) : $detail;
336 $code = ( is_array( $detail ) && ! empty( $detail['error'] ) ) ? $detail['error'] : 'api_error';
337 return new WP_Error(
338 $code,
339 $msg ?: sprintf( __( 'AI service returned an error (%d).', 'everest-forms' ), $status )
340 );
341 }
342
343 return $body;
344 }
345
346 // ── Helpers ───────────────────────────────────────────────────────────────
347
348 public static function gateway_url(): string {
349 if ( defined( 'TG_AI_GATEWAY_URL' ) ) {
350 return TG_AI_GATEWAY_URL;
351 }
352 return get_option( 'evf_ai_gateway_url', self::PRODUCTION_URL );
353 }
354
355 /**
356 * Get EVF Pro license key if the license is active — empty string otherwise.
357 * Gateway treats an empty key as free tier.
358 */
359 private static function get_license_key(): string {
360 if ( ! function_exists( 'evf_get_license_plan' ) || ! evf_get_license_plan() ) {
361 return '';
362 }
363 return (string) get_option( 'everest-forms-pro_license_key', '' );
364 }
365
366 private static function get_domain(): string {
367 return preg_replace( '(^https?://)', '', rtrim( home_url(), '/' ) );
368 }
369 }
370