-
Notifications
You must be signed in to change notification settings - Fork 154
feat: enhance AI settings with temperature control, API key masking, and connection testing #1758
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
feat: enhance AI settings with temperature control, API key masking, and connection testing #1758
Conversation
…and connection testing
WalkthroughRefactors AI provider connection testing to accept optional API key/provider/model overrides, adds provider-specific test helpers (OpenAI, Anthropic, Google), changes the REST test route to accept those args and use POST semantics, and replaces the settings API key input with a masked interactive widget plus a temperature field. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
includes/AI/FormGenerator.php (1)
873-922: Test connection with overrides properly manages state.The implementation correctly:
- Reloads settings to get latest data
- Stores original values before overriding
- Validates API key presence
- Restores original values after testing
- Handles exceptions with proper error response
However, the
catchblock doesn't restore original values on exception.The exception handler should restore original values to maintain consistency:
} catch (\Exception $e) { + // Restore original values on exception + $this->current_provider = $original_provider; + $this->current_model = $original_model; + $this->api_key = $original_key; + return [ 'success' => false, 'provider' => $this->current_provider, 'message' => $e->getMessage() ]; }
🧹 Nitpick comments (1)
includes/functions/settings-options.php (1)
811-814: Hidden inputs expose all provider API keys in page source.All three provider API keys are stored in hidden input fields in the HTML source. While they're not directly visible, they can be viewed via browser dev tools or "View Source". This is acceptable for an admin-only settings page but worth noting.
Consider fetching non-current provider keys via AJAX only when the provider is switched, rather than embedding all keys in the DOM. This reduces the attack surface if an admin's browser session is compromised.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
includes/AI/FormGenerator.php(3 hunks)includes/AI/RestController.php(2 hunks)includes/functions/settings-options.php(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
includes/AI/RestController.php (1)
includes/AI/FormGenerator.php (1)
test_connection(873-923)
includes/AI/FormGenerator.php (2)
Lib/WeDevs_Settings_API.php (1)
get_option(616-625)includes/AI/RestController.php (1)
test_connection(405-423)
includes/functions/settings-options.php (1)
Lib/WeDevs_Settings_API.php (1)
get_option(616-625)
🪛 PHPMD (2.15.0)
includes/functions/settings-options.php
1265-1265: Avoid unused parameters such as '$args'. (undefined)
(UnusedFormalParameter)
🔇 Additional comments (12)
includes/functions/settings-options.php (6)
650-656: LGTM - Field definition for API key with callback.The field configuration correctly uses the callback type to render a custom API key UI component with masking and test connection functionality.
667-673: LGTM - Temperature field configuration.The temperature field is properly defined with callback type and sensible default of 0.7.
757-769: API key masking logic is sound.The masking implementation correctly shows first 4 and last 4 characters for keys longer than 8 characters, and fully masks shorter keys. This provides good UX while maintaining security.
1014-1026: AJAX test connection properly uses REST API with nonce.The implementation correctly:
- Uses POST method for the test endpoint
- Sends JSON content type
- Includes WP REST nonce via
X-WP-Nonceheader- Passes provider, API key, and model for testing
1265-1285: Temperature field implementation is correct; unused parameter is acceptable.The static analysis flags
$argsas unused, but this is a required signature for WordPress Settings API callbacks. The implementation properly clamps values between 0.0 and 1.0 and uses HTML5 validation attributes.
790-798: Verify that proper capability checks are in place before this settings page is rendered.While
esc_attr()is properly used on the API key value, confirming admin-only access with capability checks (such ascurrent_user_can()with appropriate capabilities) is essential to validate the claim that the XSS risk is minimal in this context.includes/AI/RestController.php (2)
125-146: Good: POST method for test endpoint with optional parameters.Changing from READABLE to CREATABLE (GET to POST) is the correct approach since:
- The request includes sensitive API keys that shouldn't be in URL/logs
- The operation has side effects (makes external API calls)
- Parameters are properly typed and sanitized
The
sanitize_callbackforapi_keyusessanitize_text_fieldwhich is appropriate for API key strings.
405-423: LGTM - Test connection handler correctly passes parameters.The handler properly extracts parameters from the request and forwards them to
FormGenerator::test_connection(). The try-catch ensures errors are handled gracefully.includes/AI/FormGenerator.php (4)
98-116: Good: Temperature defaults properly loaded from settings.The implementation correctly:
- Loads settings with
get_option('wpuf_ai', [])- Only sets temperature if not already provided in options
- Uses
floatval()for type safety with fallback to 0.7
932-950: LGTM - Provider dispatcher with clean switch statement.The
test_provider_api()method cleanly routes to provider-specific test methods and handles unknown providers gracefully.
955-995: OpenAI test connection is well-implemented.The method uses a minimal request (
max_tokens: 5, simple "Hi" message) to minimize API costs while still validating credentials. Error handling properly extracts the error message from the response body.
1000-1041: Anthropic test connection follows same pattern - good consistency.The implementation mirrors OpenAI's approach with appropriate Anthropic-specific headers (
x-api-key,anthropic-version).
|
Token limit configuration is missing @arifulhoque7 vai |
Token limit configuration can cause broken JSON in the form builder, thus we can't implement it here @Rubaiyat-E-Mohammad vai |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- use sprintf() to make translatable string with placeholder like urls, html attributes etc.
- follow our projects coding standard for spacing, indentation etc.
| [ | ||
| 'name' => 'api_key_current', | ||
| 'label' => __( 'API Key', 'wp-user-frontend' ), | ||
| 'desc' => __( 'Enter your AI service API key. Need help finding your <a href="https://platform.openai.com/api-keys" target="_blank" class="wpuf-api-key-link" data-openai="https://platform.openai.com/api-keys" data-anthropic="https://console.anthropic.com/settings/keys" data-google="https://aistudio.google.com/app/apikey" style="text-decoration: underline;">API Key?</a>', 'wp-user-frontend' ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use sprintf() to make translatable string with placeholder like urls, html attributes etc.
Refactored AI provider connection test methods for better code consistency and error handling. Updated the API key settings field to use sprintf for localized help links, improving translation and maintainability.
done @sapayth vai |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
includes/AI/FormGenerator.php (1)
567-599: Avoid sending Google API key in query string for main generation call
generate_with_google()still appends the API key as a?key=query parameter on$endpoint, whiletest_google_connection()correctly uses thex-goog-api-keyheader. For server‑side code, using the header for both is preferable to avoid exposing keys in logs and intermediaries.Refactor
generate_with_google()to remove'?key=' . $this->api_keyfrom the URL and instead includex-goog-api-keyin theheadersarray, mirroringtest_google_connection().
🧹 Nitpick comments (3)
includes/AI/FormGenerator.php (2)
873-923: Improve robustness of state restoration intest_connectionThe overall flow (reload settings, allow overrides, temporarily swap API key, then delegate to
test_provider_api()) looks good. However, if an exception is ever thrown after$this->current_provider/$this->current_model/$this->api_keyare overridden, the catch block returns an error but never restores the originals, unlikegenerate_form().Wrapping restoration in a
finally-style pattern (or duplicating restoration in the catch) would make this more future‑proof and consistent withgenerate_form()’s restoration logic.
955-995: Connection test helpers are logically correct; adjust array formatting to satisfy PHPCS
- The three test helpers correctly:
- Use provider‑appropriate endpoints and auth headers.
- Send a minimal 5‑token request.
- Normalize responses into a
{ success, provider, message }structure.- Static hint at ~964: in
test_openai_connection(), the multi‑item associative array that includes'messages'should be expanded so each value starts on a new line to match the project’s coding standard (e.g., break[ [ 'role' => 'user', 'content' => 'Hi' ] ],across lines).This is cosmetic but will clear the PHPCS warning.
Also applies to: 1000-1041, 1046-1091
includes/functions/settings-options.php (1)
976-991: Temperature field implementation is sound; keep$argsand server‑side clamping
- The temperature input is correctly configured as an HTML5
<input type="number">withmin="0",max="1", andstep="0.1", and the JS adds nice immediate visual feedback.wpuf_ai_temperature_field()clamps the value to[0.0, 1.0]before output, which is good defense in depth and aligns with the backend usage inFormGenerator::generate_form().The
$argsparameter is intentionally unused but required by the Settings API callback signature. If PHPMD’s warning becomes noisy, a minimal fix is to rename it to$_argsor add a short comment (// Required by Settings API callback signature) to make the intent clear.Also applies to: 1274-1294
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
includes/AI/FormGenerator.php(2 hunks)includes/functions/settings-options.php(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
includes/AI/FormGenerator.php (2)
Lib/WeDevs_Settings_API.php (1)
get_option(616-625)includes/AI/RestController.php (1)
test_connection(405-423)
includes/functions/settings-options.php (1)
Lib/WeDevs_Settings_API.php (1)
get_option(616-625)
🪛 GitHub Check: Run PHPCS inspection
includes/AI/FormGenerator.php
[failure] 99-99:
Expected 1 spaces before closing parenthesis; 0 found
[failure] 99-99:
Expected 1 spaces after opening parenthesis; 0 found
[warning] 964-964:
When a multi-item array uses associative keys, each value should start on a new line.
🪛 PHPMD (2.15.0)
includes/functions/settings-options.php
1274-1274: Avoid unused parameters such as '$args'. (undefined)
(UnusedFormalParameter)
🔇 Additional comments (3)
includes/AI/FormGenerator.php (1)
932-950: Provider dispatch helper looks correct and future‑proof
test_provider_api()cleanly dispatches by$this->current_providerand returns a standardized array, with an explicit “Unknown provider” branch. This is a nice central hook for additional providers later.includes/functions/settings-options.php (2)
650-682: AI settings field definitions look consistent with intended UXThe new
api_key_currentcallback field andtemperaturecallback field are wired correctly (names, defaults, callbacks, descriptions), and the order (provider → key → model → temperature) matches the PR objective.
766-819: FixTest Connectionbehavior for already‑saved keys and align model gatingThe masking UI and per‑provider hidden key storage are well thought out, but there's a functional gap:
- On initial load, when an API key already exists:
- The masked display shows correctly.
#wpuf_ai_api_key_fieldis empty and hidden.updateModelFieldState()is initialized withinitialApiKey = $('#wpuf_ai_api_key_field').val();(empty), so the model select is disabled and its description changed to "Please enter a valid API key…".- The Test Connection button uses
var apiKey = $('#wpuf_ai_api_key_field').val();(Line 1001–1002), so clicking it with a saved key but without pressing "Change" always yields "Please enter a valid API key", even though a valid key exists in the hidden input.To make the UX match the spec ("test" should work with the configured key without forcing a change), I recommend:
- Deriving the key for testing and model gating from the actual stored value, e.g.:
// When testing var provider = $('input[name="wpuf_ai[ai_provider]"]:checked').val(); var apiKey = $('#wpuf_ai_' + provider + '_key').val() || $('#wpuf_ai_api_key_field').val();
- Initializing state from the current provider's hidden key:
var initialProvider = $('input[name="wpuf_ai[ai_provider]"]:checked').val() || 'openai'; updateVisibleInputName(initialProvider); updateApiKeyDisplay(initialProvider); var initialApiKey = $('#wpuf_ai_' + initialProvider + '_key').val(); updateModelFieldState(initialApiKey); $('#wpuf-test-connection-btn').prop('disabled', !initialApiKey || initialApiKey.trim().length < 10);This keeps the masking behavior but lets admins test existing keys and have the model field correctly enabled when a valid key is already stored.
| // Using direct API implementation for AI providers | ||
|
|
||
| // Use manual AI provider implementation as fallback | ||
| switch ($this->current_provider) { | ||
| case 'openai': | ||
| $result = $this->generate_with_openai($prompt, $options); | ||
| break; | ||
|
|
||
| case 'anthropic': | ||
| $result = $this->generate_with_anthropic($prompt, $options); | ||
| break; | ||
|
|
||
| case 'google': | ||
| $result = $this->generate_with_google($prompt, $options); | ||
| break; | ||
|
|
||
| default: | ||
| throw new \Exception('Unsupported AI provider: ' . $this->current_provider); | ||
| } | ||
|
|
||
| // Restore original provider, model, and API key | ||
| $this->current_provider = $original_provider; | ||
| $this->current_model = $original_model; | ||
| $this->api_key = $original_api_key; | ||
|
|
||
| return $result; | ||
|
|
||
| } catch (\Exception $e) { | ||
| // Ensure full restoration even on exception | ||
| $this->current_provider = $original_provider ?? $this->current_provider; | ||
| $this->current_model = $original_model ?? $this->current_model; | ||
| $this->api_key = $original_api_key ?? $this->api_key; | ||
|
|
||
| return [ | ||
| 'success' => false, | ||
| 'error' => true, | ||
| 'message' => $e->getMessage(), | ||
| 'provider' => $this->current_provider | ||
| ]; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get model-specific parameter configuration | ||
| * | ||
| * This centralizes all model-specific parameter mappings for easy maintenance. | ||
| * Different AI providers and models have different parameter requirements: | ||
| * | ||
| * - Token parameters: 'max_tokens' vs 'max_completion_tokens' vs 'maxOutputTokens' | ||
| * - Temperature: Some models only support default temperature (1.0) | ||
| * - Response format: Some models don't support JSON mode | ||
| * | ||
| * @since 4.2.1 | ||
| * | ||
| * @param string $provider Provider name (openai, anthropic, google) | ||
| * @param string $model Model name (e.g., gpt-5, claude-4.1-opus, gemini-2.5-pro) | ||
| * @return array Model configuration with parameter restrictions and requirements | ||
| */ | ||
| private function get_model_config($provider, $model) { | ||
| // Get configuration from centralized Config class | ||
| $config = Config::get_model_config($model); | ||
|
|
||
| // Return config if found, otherwise use safe defaults | ||
| if ($config !== null) { | ||
| return $config; | ||
| } | ||
|
|
||
| // Fallback defaults for each provider if model not found | ||
| $defaults = [ | ||
| 'openai' => [ | ||
| 'token_param' => 'max_tokens', | ||
| 'token_location' => 'body', | ||
| 'supports_json_mode' => true, | ||
| 'supports_custom_temperature' => true | ||
| ], | ||
| 'anthropic' => [ | ||
| 'token_param' => 'max_tokens', | ||
| 'token_location' => 'body', | ||
| 'supports_json_mode' => true, | ||
| 'supports_custom_temperature' => true | ||
| ], | ||
| 'google' => [ | ||
| 'token_param' => 'maxOutputTokens', | ||
| 'token_location' => 'generationConfig', | ||
| 'supports_json_mode' => true, | ||
| 'supports_custom_temperature' => true | ||
| ] | ||
| ]; | ||
|
|
||
| return $defaults[$provider] ?? $defaults['openai']; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Generate form using OpenAI | ||
| * | ||
| * @since 4.2.1 | ||
| * | ||
| * @param string $prompt User prompt | ||
| * @param array $options Additional options | ||
| * @return array Generated form data | ||
| */ | ||
| private function generate_with_openai($prompt, $options = []) { | ||
| $context = $options['conversation_context'] ?? []; | ||
| $form_type = $options['form_type'] ?? 'post'; | ||
| $language = $options['language'] ?? 'English'; | ||
|
|
||
| // Add language to context for system prompt | ||
| $context['language'] = $language; | ||
|
|
||
| $system_prompt = $this->get_system_prompt($context, $form_type); | ||
|
|
||
| // Get model-specific configuration | ||
| $model_config = $this->get_model_config('openai', $this->current_model); | ||
|
|
||
| $body = [ | ||
| 'model' => $this->current_model, | ||
| 'messages' => [ | ||
| ['role' => 'system', 'content' => $system_prompt], | ||
| ['role' => 'user', 'content' => $prompt] | ||
| ] | ||
| ]; | ||
|
|
||
| // Set temperature based on model capabilities | ||
| if ($model_config['supports_custom_temperature']) { | ||
| $body['temperature'] = floatval($options['temperature'] ?? 0.7); | ||
| } else { | ||
| // Use fixed temperature for models that don't support custom temperature | ||
| $body['temperature'] = $model_config['temperature'] ?? 1.0; | ||
| } | ||
|
|
||
| // Set response format based on model capabilities | ||
| if ($model_config['supports_json_mode']) { | ||
| $body['response_format'] = ['type' => 'json_object']; | ||
| } else { | ||
| // For models that don't support JSON mode, add explicit instruction to system prompt | ||
| $system_prompt .= "\n\nIMPORTANT: You must respond with ONLY valid JSON. Do not include any explanatory text, markdown formatting, or code blocks. Return ONLY the JSON object."; | ||
| // Update the message payload with the modified system prompt | ||
| $body['messages'][0]['content'] = $system_prompt; | ||
| } | ||
|
|
||
| // Set token parameter based on model | ||
| if ($model_config['token_location'] === 'body') { | ||
| // GPT-5 needs significantly more tokens for reasoning + output | ||
| if (strpos($this->current_model, 'gpt-5') === 0) { | ||
| $body[$model_config['token_param']] = intval($options['max_tokens'] ?? 65536); | ||
| } else { | ||
| $body[$model_config['token_param']] = intval($options['max_tokens'] ?? 2000); | ||
| } | ||
| } | ||
|
|
||
| $args = [ | ||
| 'method' => 'POST', | ||
| 'headers' => [ | ||
| 'Authorization' => 'Bearer ' . $this->api_key, | ||
| 'Content-Type' => 'application/json' | ||
| ], | ||
| 'body' => json_encode($body), | ||
| 'timeout' => 120 | ||
| ]; | ||
|
|
||
| $response = wp_safe_remote_request($this->provider_configs['openai']['endpoint'], $args); | ||
|
|
||
| if (is_wp_error($response)) { | ||
| $error_message = $response->get_error_message(); | ||
|
|
||
| // Check for specific timeout errors | ||
| if (strpos($error_message, 'timeout') !== false || strpos($error_message, 'timed out') !== false) { | ||
| throw new \Exception('OpenAI API request timed out. Please try again later.'); | ||
| } | ||
|
|
||
| throw new \Exception('OpenAI API request failed: ' . $error_message); | ||
| } | ||
|
|
||
| $status_code = wp_remote_retrieve_response_code($response); | ||
| if ($status_code !== 200) { | ||
| $error_body = wp_remote_retrieve_body($response); | ||
| throw new \Exception("OpenAI API returned HTTP {$status_code}: {$error_body}"); | ||
| } | ||
|
|
||
| $body = wp_remote_retrieve_body($response); | ||
| $data = json_decode($body, true); | ||
|
|
||
| // Validate JSON response | ||
| if (json_last_error() !== JSON_ERROR_NONE) { | ||
| throw new \Exception('Invalid JSON response from AI provider: ' . json_last_error_msg()); | ||
| } | ||
|
|
||
| if (isset($data['error'])) { | ||
| throw new \Exception('OpenAI API Error: ' . $data['error']['message']); | ||
| } | ||
|
|
||
| if (!isset($data['choices'][0]['message']['content'])) { | ||
| throw new \Exception('Invalid OpenAI response format. Response: ' . json_encode($data)); | ||
| } | ||
|
|
||
| $content = $data['choices'][0]['message']['content']; | ||
|
|
||
| // Check for empty response | ||
| if (empty($content)) { | ||
| // Return error response instead of fallback | ||
| return [ | ||
| 'success' => false, | ||
| 'error' => true, | ||
| 'message' => 'AI model returned empty response. Please try again.', | ||
| 'provider' => 'openai', | ||
| 'model' => $this->current_model | ||
| ]; | ||
| } | ||
|
|
||
| // Clean and extract JSON from the response | ||
| $json_content = trim($content); | ||
|
|
||
| // Remove any markdown code blocks if present | ||
| $json_content = preg_replace('/^```(?:json)?\s*|\s*```$/m', '', $json_content); | ||
|
|
||
| // Remove any text before the first { or after the last } | ||
| $json_content = preg_replace('/^[^{]*/', '', $json_content); | ||
| $json_content = preg_replace('/[^}]*$/', '', $json_content); | ||
|
|
||
| // Try to find the JSON object (handle nested braces properly) | ||
| $start = strpos($json_content, '{'); | ||
| $end = strrpos($json_content, '}'); | ||
|
|
||
| if ($start !== false && $end !== false && $end > $start) { | ||
| $json_content = substr($json_content, $start, $end - $start + 1); | ||
| } | ||
|
|
||
| // Attempt to decode JSON | ||
| $ai_response = json_decode($json_content, true); | ||
|
|
||
| if (json_last_error() !== JSON_ERROR_NONE) { | ||
| return [ | ||
| 'success' => false, | ||
| 'error' => true, | ||
| 'message' => 'Unable to generate form. Please try again or rephrase your request.', | ||
| 'provider' => 'openai', | ||
| 'model' => $this->current_model | ||
| ]; | ||
| } | ||
|
|
||
| // Check for error response from AI | ||
| if ( ! empty( $ai_response['error'] ) ) { | ||
| return [ | ||
| 'success' => false, | ||
| 'error' => true, | ||
| 'message' => $ai_response['message'] ?? 'AI returned an error response', | ||
| 'provider' => 'openai', | ||
| 'model' => $this->current_model | ||
| ]; | ||
| } | ||
|
|
||
| // Build complete form from minimal AI response | ||
| $form_data = Form_Builder::build_form( $ai_response ); | ||
|
|
||
| // Check if form building failed | ||
| if ( ! empty( $form_data['error'] ) ) { | ||
| return [ | ||
| 'success' => false, | ||
| 'error' => true, | ||
| 'message' => $form_data['message'] ?? 'Failed to build form structure', | ||
| 'provider' => 'openai', | ||
| 'model' => $this->current_model | ||
| ]; | ||
| } | ||
|
|
||
| // Add metadata with better uniqueness | ||
| $timestamp = microtime(true); | ||
| $random = bin2hex(random_bytes(5)); | ||
| $form_data['session_id'] = $options['session_id'] ?? 'wpuf_ai_session_' . $timestamp . '_' . $random; | ||
| $form_data['response_id'] = 'openai_resp_' . $timestamp . '_' . $random; | ||
| $form_data['provider'] = 'openai'; | ||
| $form_data['model'] = $this->current_model; | ||
| $form_data['generated_at'] = current_time('mysql'); | ||
| $form_data['success'] = true; | ||
|
|
||
| return $form_data; | ||
| } | ||
|
|
||
| /** | ||
| * Generate form using Anthropic Claude | ||
| * | ||
| * @since 4.2.1 | ||
| * | ||
| * @param string $prompt User prompt | ||
| * @param array $options Additional options | ||
| * @return array Generated form data | ||
| */ | ||
| private function generate_with_anthropic($prompt, $options = []) { | ||
| $context = $options['conversation_context'] ?? []; | ||
| $form_type = $options['form_type'] ?? 'post'; | ||
| $language = $options['language'] ?? 'English'; | ||
|
|
||
| // Add language to context for system prompt | ||
| $context['language'] = $language; | ||
|
|
||
| $system_prompt = $this->get_system_prompt($context, $form_type); | ||
|
|
||
| // Get model-specific configuration | ||
| $model_config = $this->get_model_config('anthropic', $this->current_model); | ||
|
|
||
| $body = [ | ||
| 'model' => $this->current_model, | ||
| 'system' => $system_prompt, | ||
| 'messages' => [ | ||
| ['role' => 'user', 'content' => $prompt] | ||
| ] | ||
| ]; | ||
|
|
||
| // Set temperature based on model capabilities | ||
| if ($model_config['supports_custom_temperature']) { | ||
| $body['temperature'] = floatval($options['temperature'] ?? 0.7); | ||
| } else { | ||
| // Use fixed temperature for models that don't support custom temperature | ||
| $body['temperature'] = $model_config['temperature'] ?? 1.0; | ||
| } | ||
|
|
||
| // Set token parameter based on model | ||
| if ($model_config['token_location'] === 'body') { | ||
| // GPT-5 needs significantly more tokens for reasoning + output | ||
| if (strpos($this->current_model, 'gpt-5') === 0) { | ||
| $body[$model_config['token_param']] = intval($options['max_tokens'] ?? 65536); | ||
| } else { | ||
| $body[$model_config['token_param']] = intval($options['max_tokens'] ?? 2000); | ||
| } | ||
| } | ||
|
|
||
| $args = [ | ||
| 'method' => 'POST', | ||
| 'headers' => [ | ||
| 'x-api-key' => $this->api_key, | ||
| 'anthropic-version' => '2023-06-01', | ||
| 'Content-Type' => 'application/json' | ||
| ], | ||
| 'body' => json_encode($body), | ||
| 'timeout' => 120 | ||
| ]; | ||
|
|
||
| $response = wp_safe_remote_request($this->provider_configs['anthropic']['endpoint'], $args); | ||
|
|
||
| if (is_wp_error($response)) { | ||
| throw new \Exception('Anthropic API request failed: ' . $response->get_error_message()); | ||
| } | ||
|
|
||
| $status_code = wp_remote_retrieve_response_code($response); | ||
| if ($status_code !== 200) { | ||
| $error_body = wp_remote_retrieve_body($response); | ||
| throw new \Exception("Anthropic API returned HTTP {$status_code}: {$error_body}"); | ||
| } | ||
|
|
||
| $body = wp_remote_retrieve_body($response); | ||
| $data = json_decode($body, true); | ||
|
|
||
| // Validate JSON response | ||
| if (json_last_error() !== JSON_ERROR_NONE) { | ||
| throw new \Exception('Invalid JSON response from Anthropic API: ' . json_last_error_msg()); | ||
| } | ||
|
|
||
| if (isset($data['error'])) { | ||
| throw new \Exception('Anthropic API Error: ' . $data['error']['message']); | ||
| } | ||
|
|
||
| if (!isset($data['content'][0]['text'])) { | ||
| throw new \Exception('Invalid Anthropic response format'); | ||
| } | ||
|
|
||
| $content = $data['content'][0]['text']; | ||
|
|
||
| // Clean and extract JSON from the response (Claude may include explanatory text) | ||
| $json_content = trim($content); | ||
| $json_content = preg_replace('/^```(?:json)?\s*|\s*```$/m', '', $json_content); | ||
| $json_content = preg_replace('/^[^{]*/', '', $json_content); | ||
| $json_content = preg_replace('/[^}]*$/', '', $json_content); | ||
|
|
||
| $start = strpos($json_content, '{'); | ||
| $end = strrpos($json_content, '}'); | ||
| if ($start !== false && $end !== false && $end > $start) { | ||
| $json_content = substr($json_content, $start, $end - $start + 1); | ||
| } | ||
|
|
||
| // Decode JSON | ||
| $ai_response = json_decode($json_content, true); | ||
| if (json_last_error() !== JSON_ERROR_NONE) { | ||
| throw new \Exception('Unable to generate form. Please try again or rephrase your request.'); | ||
| } | ||
|
|
||
| // Check for error response from AI | ||
| if ( ! empty( $ai_response['error'] ) ) { | ||
| return [ | ||
| 'success' => false, | ||
| 'error' => true, | ||
| 'message' => $ai_response['message'] ?? 'AI returned an error response', | ||
| 'provider' => 'anthropic', | ||
| 'model' => $this->current_model | ||
| ]; | ||
| } | ||
|
|
||
| // Build complete form from minimal AI response | ||
| $form_data = Form_Builder::build_form( $ai_response ); | ||
|
|
||
| // Check if form building failed | ||
| if ( ! empty( $form_data['error'] ) ) { | ||
| return [ | ||
| 'success' => false, | ||
| 'error' => true, | ||
| 'message' => $form_data['message'] ?? 'Failed to build form structure', | ||
| 'provider' => 'anthropic', | ||
| 'model' => $this->current_model | ||
| ]; | ||
| } | ||
|
|
||
| // Add metadata | ||
| $timestamp = microtime(true); | ||
| $random = bin2hex(random_bytes(5)); | ||
| $form_data['session_id'] = $options['session_id'] ?? 'wpuf_ai_session_' . $timestamp . '_' . $random; | ||
| $form_data['response_id'] = 'anthropic_resp_' . $timestamp . '_' . $random; | ||
| $form_data['provider'] = 'anthropic'; | ||
| $form_data['model'] = $this->current_model; | ||
| $form_data['generated_at'] = current_time('mysql'); | ||
| $form_data['success'] = true; | ||
|
|
||
| return $form_data; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Generate form using Google Gemini | ||
| * | ||
| * @since 4.2.1 | ||
| * | ||
| * @param string $prompt User prompt | ||
| * @param array $options Additional options | ||
| * @return array Generated form data | ||
| */ | ||
| private function generate_with_google($prompt, $options = []) { | ||
| $context = $options['conversation_context'] ?? []; | ||
| $form_type = $options['form_type'] ?? 'post'; | ||
| $language = $options['language'] ?? 'English'; | ||
|
|
||
| // Add language to context for system prompt | ||
| $context['language'] = $language; | ||
|
|
||
| $system_prompt = $this->get_system_prompt($context, $form_type); | ||
|
|
||
| // Get model-specific configuration | ||
| $model_config = $this->get_model_config('google', $this->current_model); | ||
|
|
||
| // Build endpoint with model | ||
| $endpoint = str_replace('{model}', $this->current_model, $this->provider_configs['google']['endpoint']); | ||
| $endpoint .= '?key=' . $this->api_key; | ||
|
|
||
| $body = [ | ||
| 'contents' => [ | ||
| [ | ||
| 'parts' => [ | ||
| ['text' => $system_prompt . "\n\nUser request: " . $prompt] | ||
| ] | ||
| ] | ||
| ], | ||
| 'generationConfig' => [] | ||
| ]; | ||
|
|
||
| // Set temperature based on model capabilities | ||
| if ($model_config['supports_custom_temperature']) { | ||
| $body['generationConfig']['temperature'] = floatval($options['temperature'] ?? 0.7); | ||
| } else { | ||
| // Use fixed temperature for models that don't support custom temperature | ||
| $body['generationConfig']['temperature'] = $model_config['temperature'] ?? 1.0; | ||
| } | ||
|
|
||
| // Set response format based on model capabilities | ||
| if ($model_config['supports_json_mode']) { | ||
| $body['generationConfig']['responseMimeType'] = 'application/json'; | ||
| } | ||
|
|
||
| // Set token parameter based on model | ||
| if ($model_config['token_location'] === 'generationConfig') { | ||
| $body['generationConfig'][$model_config['token_param']] = intval($options['max_tokens'] ?? 2000); | ||
| } | ||
|
|
||
| $args = [ | ||
| 'method' => 'POST', | ||
| 'headers' => [ | ||
| 'Content-Type' => 'application/json' | ||
| ], | ||
| 'body' => json_encode($body), | ||
| 'timeout' => 120 | ||
| ]; | ||
|
|
||
| $response = wp_safe_remote_request($endpoint, $args); | ||
|
|
||
| if (is_wp_error($response)) { | ||
| throw new \Exception('Google API request failed: ' . $response->get_error_message()); | ||
| } | ||
|
|
||
| $status_code = wp_remote_retrieve_response_code($response); | ||
| if ($status_code !== 200) { | ||
| $error_body = wp_remote_retrieve_body($response); | ||
| throw new \Exception("Google API returned HTTP {$status_code}: {$error_body}"); | ||
| } | ||
|
|
||
| $body = wp_remote_retrieve_body($response); | ||
| $data = json_decode($body, true); | ||
|
|
||
| // Validate JSON response | ||
| if (json_last_error() !== JSON_ERROR_NONE) { | ||
| throw new \Exception('Invalid JSON response from Google API: ' . json_last_error_msg()); | ||
| } | ||
|
|
||
| if (isset($data['error'])) { | ||
| throw new \Exception('Google API Error: ' . ($data['error']['message'] ?? 'Unknown error')); | ||
| } | ||
|
|
||
| if (!isset($data['candidates'][0]['content']['parts'][0]['text'])) { | ||
| throw new \Exception('Invalid Google response format'); | ||
| } | ||
|
|
||
| $content = $data['candidates'][0]['content']['parts'][0]['text']; | ||
|
|
||
| // Clean and extract JSON from content | ||
| $json_content = trim($content); | ||
|
|
||
| // Remove any markdown code blocks if present | ||
| $json_content = preg_replace('/^```(?:json)?\s*|\s*```$/m', '', $json_content); | ||
|
|
||
| // Remove any text before the first { or after the last } | ||
| $json_content = preg_replace('/^[^{]*/', '', $json_content); | ||
| $json_content = preg_replace('/[^}]*$/', '', $json_content); | ||
|
|
||
| // Try to find the JSON object (handle nested braces properly) | ||
| $start = strpos($json_content, '{'); | ||
| $end = strrpos($json_content, '}'); | ||
|
|
||
| if ($start !== false && $end !== false && $end > $start) { | ||
| $json_content = substr($json_content, $start, $end - $start + 1); | ||
| } | ||
|
|
||
| // Attempt to decode JSON | ||
| $ai_response = json_decode($json_content, true); | ||
|
|
||
| if (json_last_error() !== JSON_ERROR_NONE) { | ||
| throw new \Exception('Unable to generate form. Please try again or rephrase your request.'); | ||
| } | ||
|
|
||
| // Check for error response from AI | ||
| if ( ! empty( $ai_response['error'] ) ) { | ||
| return [ | ||
| 'success' => false, | ||
| 'error' => true, | ||
| 'message' => $ai_response['message'] ?? 'AI returned an error response', | ||
| 'provider' => 'google', | ||
| 'model' => $this->current_model | ||
| ]; | ||
| } | ||
|
|
||
| // Build complete form from minimal AI response | ||
| $form_data = Form_Builder::build_form( $ai_response ); | ||
|
|
||
| // Check if form building failed | ||
| if ( ! empty( $form_data['error'] ) ) { | ||
| return [ | ||
| 'success' => false, | ||
| 'error' => true, | ||
| 'message' => $form_data['message'] ?? 'Failed to build form structure', | ||
| 'provider' => 'google', | ||
| 'model' => $this->current_model | ||
| ]; | ||
| } | ||
|
|
||
| // Add metadata with better uniqueness | ||
| $timestamp = microtime(true); | ||
| $random = bin2hex(random_bytes(5)); | ||
| $form_data['session_id'] = $options['session_id'] ?? 'wpuf_ai_session_' . $timestamp . '_' . $random; | ||
| $form_data['response_id'] = 'google_resp_' . $timestamp . '_' . $random; | ||
| $form_data['provider'] = 'google'; | ||
| $form_data['model'] = $this->current_model; | ||
| $form_data['generated_at'] = current_time('mysql'); | ||
| $form_data['success'] = true; | ||
|
|
||
| return $form_data; | ||
| } | ||
|
|
||
| /** | ||
| * Get system prompt for AI form generation | ||
| * | ||
| * @since 4.2.1 | ||
| * | ||
| * @param array $context Conversation context | ||
| * @param string $form_type Form type ('post' or 'profile') | ||
| * @return string System prompt | ||
| */ | ||
| private function get_system_prompt($context = [], $form_type = 'post') { | ||
| // Determine which prompt file to use based on form type | ||
| if ( 'profile' === $form_type || 'registration' === $form_type ) { | ||
| // Registration/Profile form prompt - USE MINIMAL REGISTRATION PROMPT | ||
| $prompt_file = WPUF_ROOT . '/includes/AI/wpuf-ai-minimal-prompt-registration.md'; | ||
| } else { | ||
| // Post form prompt - USE MINIMAL PROMPT | ||
| $prompt_file = WPUF_ROOT . '/includes/AI/wpuf-ai-minimal-prompt.md'; | ||
| } | ||
|
|
||
| // Check if file exists | ||
| if ( ! file_exists( $prompt_file ) ) { | ||
| throw new \Exception( 'System prompt file not found: ' . $prompt_file ); | ||
| } | ||
|
|
||
| // Load the prompt file | ||
| $system_prompt = file_get_contents( $prompt_file ); | ||
|
|
||
| // Add form type context (informational, not restrictive) | ||
| $system_prompt .= "\n\n## FORM TYPE CONTEXT\n"; | ||
| if ( 'profile' === $form_type || 'registration' === $form_type ) { | ||
| $system_prompt .= "You are working with a REGISTRATION/PROFILE form.\n"; | ||
| $system_prompt .= "- Use registration/profile fields: user_email, user_login, password, first_name, last_name, biography, user_avatar, social fields, phone_field, address_field, dropdown_field, radio_field, checkbox_field, etc.\n"; | ||
| $system_prompt .= "- Custom fields like dropdown, radio, checkbox, text fields are fully supported for additional profile information\n"; | ||
| $system_prompt .= "- Focus on helping users collect user registration and profile data\n"; | ||
| } else { | ||
| $system_prompt .= "You are working with a POST submission form.\n"; | ||
| $system_prompt .= "- Use post fields: post_title, post_content, post_excerpt, featured_image, taxonomy, custom fields, etc.\n"; | ||
| $system_prompt .= "- Focus on helping users collect content submission data\n"; | ||
| } | ||
|
|
||
| // Add language context if provided or extract from user message | ||
| $target_language = $context['language'] ?? 'English'; | ||
|
|
||
| // Extract language from user message if they're requesting conversion/translation | ||
| if ( ! empty( $context['chat_history'] ) && is_array( $context['chat_history'] ) && count( $context['chat_history'] ) > 0 ) { | ||
| $last_message = end( $context['chat_history'] ); | ||
| $last_user_message = $last_message['content'] ?? ''; | ||
|
|
||
| // Detect language conversion/translation requests | ||
| if ( preg_match( '/(?:convert|translate|change|make).*?(?:to|in|into)\s+(\w+)/i', $last_user_message, $matches ) ) { | ||
| $detected_language = ucfirst( strtolower( $matches[1] ) ); | ||
| $target_language = $detected_language; | ||
| } | ||
| } | ||
|
|
||
| if ( ! empty( $target_language ) && $target_language !== 'English' ) { | ||
| $system_prompt .= "\n\n## TARGET LANGUAGE\n"; | ||
| $system_prompt .= "**CRITICAL: The user has selected '{$target_language}' as their target language.**\n"; | ||
| $system_prompt .= "- Generate ALL field labels in {$target_language}\n"; | ||
| $system_prompt .= "- Generate ALL field placeholders in {$target_language}\n"; | ||
| $system_prompt .= "- Generate ALL field help text in {$target_language}\n"; | ||
| $system_prompt .= "- Generate ALL dropdown/radio/checkbox options in {$target_language}\n"; | ||
| $system_prompt .= "- Generate form_title and form_description in {$target_language}\n"; | ||
| $system_prompt .= "- When adding new fields, use {$target_language} for all text content\n"; | ||
| $system_prompt .= "- Even if the user's message is in English, generate field content in {$target_language}\n"; | ||
| $system_prompt .= "- This is a language conversion request - update ALL existing field labels to {$target_language}\n\n"; | ||
| } | ||
|
|
||
| // Add conversation context if provided | ||
| if ( ! empty( $context ) ) { | ||
| $system_prompt .= "\n\n## CURRENT CONVERSATION CONTEXT\n"; | ||
|
|
||
| // Safely extract last user message | ||
| $last_user_message = ''; | ||
| if ( isset( $context['chat_history'] ) && is_array( $context['chat_history'] ) && count( $context['chat_history'] ) > 0 ) { | ||
| $last_message = end( $context['chat_history'] ); | ||
| $last_user_message = $last_message['content'] ?? ''; | ||
| } | ||
|
|
||
| // Determine modification intent | ||
| $modification_requested = false; | ||
| if ( isset( $context['modification_requested'] ) ) { | ||
| $modification_requested = (bool) $context['modification_requested']; | ||
| } else { | ||
| $modification_keywords = [ 'edit', 'modify', 'update', 'change', 'add', 'remove', 'delete', 'replace', 'convert', 'translate', 'make' ]; | ||
| foreach ( $modification_keywords as $keyword ) { | ||
| if ( false !== stripos( $last_user_message, $keyword ) ) { | ||
| $modification_requested = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Build MINIMAL context - only send template + label, not full structures | ||
| $context_for_ai = [ | ||
| 'session_id' => $context['session_id'] ?? '', | ||
| 'last_user_message' => $last_user_message, | ||
| 'modification_requested' => $modification_requested, | ||
| 'form_type' => $form_type, // Pass form type to AI for validation | ||
| ]; | ||
|
|
||
| // Include MINIMAL current fields (template + label + custom props only) | ||
| if ( ! empty( $context['current_form'] ) ) { | ||
| $minimal_fields = Form_Builder::extract_minimal_fields( $context['current_form'] ); | ||
| if ( ! empty( $minimal_fields ) ) { | ||
| $context_for_ai['current_fields'] = $minimal_fields; | ||
| $context_for_ai['form_title'] = $context['current_form']['form_title'] ?? ''; | ||
| $context_for_ai['form_description'] = $context['current_form']['form_description'] ?? ''; | ||
| } | ||
| } | ||
|
|
||
| $system_prompt .= json_encode( $context_for_ai, JSON_PRETTY_PRINT ); | ||
|
|
||
| // Add specific instruction for modifications | ||
| $system_prompt .= "\n\n## MODIFICATION INSTRUCTION\n"; | ||
| if ( $modification_requested ) { | ||
| $system_prompt .= "The user wants to MODIFY the existing form. You MUST:\n"; | ||
| $system_prompt .= "1. Return ALL existing fields from current_fields array (keep template + label + custom props)\n"; | ||
| $system_prompt .= "2. Apply the requested modification (add/remove/edit specific fields)\n"; | ||
| $system_prompt .= "3. Return COMPLETE field list with all fields in the 'fields' array\n"; | ||
| $system_prompt .= "4. Include form_title and form_description from context\n\n"; | ||
| $system_prompt .= "### Modification Types:\n"; | ||
| $system_prompt .= "- **Add field**: Return existing fields + new field\n"; | ||
| $system_prompt .= "- **Remove field**: Return existing fields without the specified field\n"; | ||
| $system_prompt .= "- **Edit field**: Update the field's properties (label, required, placeholder, etc.)\n"; | ||
| $system_prompt .= "- **Change field type**: Replace the field with a new template (e.g., checkbox_field → dropdown_field)\n"; | ||
| $system_prompt .= " Example: 'change skills checkbox to dropdown' → Replace checkbox_field template with dropdown_field template\n\n"; | ||
| $system_prompt .= "Examples:\n"; | ||
| $system_prompt .= "- 'add email field' with 3 existing fields → return ALL 4 fields\n"; | ||
| $system_prompt .= "- 'change skills from checkbox to dropdown' → return all fields with skills field having template: 'dropdown_field' instead of 'checkbox_field'\n"; | ||
| } else { | ||
| $system_prompt .= "The user is asking a question. Return an error response with helpful message."; | ||
| } | ||
| } | ||
|
|
||
| return $system_prompt; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Get available providers | ||
| * | ||
| * @since 4.2.1 | ||
| * | ||
| * @return array Provider configurations | ||
| */ | ||
| public function get_providers() { | ||
| return $this->provider_configs; | ||
| } | ||
|
|
||
| /** | ||
| * Get current provider | ||
| * | ||
| * @since 4.2.1 | ||
| * | ||
| * @return string Current provider | ||
| */ | ||
| public function get_current_provider() { | ||
| return $this->current_provider; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clamp temperature server‑side and address minor coding‑standards issues
- You correctly default
temperaturefromwpuf_aisettings, but it’s onlyfloatval()’d here. Consider clamping to[0.0, 1.0]at this call site as well (not just in the settings UI) so any malformed stored value can’t leak through to the provider APIs. - PHPCS hint: change
get_option('wpuf_ai', []);to WordPress style with spaces:get_option( 'wpuf_ai', [] );.
Also applies to: 113-116
🧰 Tools
🪛 GitHub Check: Run PHPCS inspection
[failure] 99-99:
Expected 1 spaces before closing parenthesis; 0 found
[failure] 99-99:
Expected 1 spaces after opening parenthesis; 0 found
🤖 Prompt for AI Agents
In includes/AI/FormGenerator.php around lines 98-100 (and similarly at 113-116)
the call to get_option should use WordPress spacing and the retrieved
temperature must be clamped server-side; change get_option('wpuf_ai', []); to
get_option( 'wpuf_ai', [] ); then when reading the temperature, cast to float
and clamp it into the range 0.0–1.0 (e.g. use min(max(floatval(...), 0.0), 1.0))
before using or passing it to provider APIs so malformed stored values cannot
leak through.


Summary
This PR enhances the AI settings page with several user-requested features to improve the configuration experience and ensure API keys are valid before use.
Changes Made
1. Temperature Field (Dynamic Control)
wpuf_ai[temperature]and used in all AI API callsFiles Modified:
includes/functions/settings-options.php(lines 666-673, 1246-1267)includes/AI/FormGenerator.php(lines 113-116)2. Masked API Key Display
AIza**********************xyz)Files Modified:
includes/functions/settings-options.php(lines 760-788, 867-900)3. Test Connection Button
Implementation:
Files Modified:
includes/AI/FormGenerator.php(lines 863-1074) - Addedtest_connection()and provider-specific test methodsincludes/AI/RestController.php(lines 125-146, 405-423) - Added REST endpoint with parametersincludes/functions/settings-options.php(lines 787-806, 984-1066) - Frontend AJAX integration4. Reorganized Field Order
New logical workflow order:
Files Modified:
includes/functions/settings-options.php(lines 640-674)Technical Details
Temperature Handling:
wpuf_ai['temperature']option0.7(balanced creativity)max(0.0, min(1.0, $value))$options['temperature']Test Connection Flow:
/wpuf/v1/ai-form-builder/testwith JSON payloadSecurity:
sanitize_text_field()check_permission()for test endpointTest Plan
Breaking Changes
None. All changes are backward compatible.
Files Changed
includes/functions/settings-options.php(210 lines modified)includes/AI/FormGenerator.php(27 lines modified)includes/AI/RestController.php(31 lines modified)Closes 1740
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.