Extending ClassifAI
Adding a New Feature
This tutorial demonstrates how to extend ClassifAI with a custom Feature using the Bias Check Extension as a practical example. You'll learn how to create a Feature that integrates with ClassifAI's architecture, leverages existing AI Providers, and provides both backend API functionality and frontend UI components.
Architecture Understanding
ClassifAI uses a three-tier architecture:
Service (e.g., Language Processing)
└── Feature (e.g., Bias Check, Excerpt Generation)
└── Provider (e.g., ChatGPT, Gemini, Azure OpenAI)
Key Concepts:
- Services: High-level groupings (LanguageProcessing, ImageProcessing, ContentRecommendation)
- Features: Specific AI capabilities that belong to a Service
- Providers: AI service implementations that Features can use
Your custom Feature will:
- Extend the
Featureabstract class - Register itself with an appropriate Service
- Use existing Providers from ClassifAI
- Provide settings UI and functionality
Prerequisites
Before starting, ensure you have:
- ClassifAI plugin installed and active
- Node.js and npm installed
- Composer installed (for PHP dependencies)
- Basic understanding of WordPress plugin development
- Familiarity with React/WordPress block editor
Step 1: Create Plugin Structure
Create your extension plugin with the following structure:
my-classifai-extension/
├── plugin.php # Main plugin file
├── includes/
│ └── class-plugin.php # Feature class
├── src/
│ ├── index.js # Editor integration
│ ├── settings.js # Settings page integration
│ └── components/ # Reusable React components
├── build/ # Compiled assets (generated)
├── package.json # JavaScript dependencies
└── README.md
Step 2: Create the Main Plugin File
Create plugin.php as your plugin's entry point:
<?php
/**
* Plugin Name: My ClassifAI Extension
* Description: Extends ClassifAI with a custom Feature
* Version: 1.0.0
* Author: Your Name
*/
namespace MyClassifAIExtension;
/**
* Register the Feature with ClassifAI.
*
* @param array $features The Features to register.
* @return array The Features to register.
*/
function register_feature( array $features ): array {
require_once __DIR__ . '/includes/class-plugin.php';
$features[] = Plugin::class;
return $features;
}
// Hook into the appropriate service.
// Options: language_processing_features, image_processing_features, content_recommendation_features
add_filter( 'language_processing_features', __NAMESPACE__ . '\register_feature' );
Key Points:
- Use a unique namespace for your extension
- The filter name determines which Service your Feature belongs to
- Require your Feature class file before adding it to the features array
Step 3: Create the Feature Class
Create includes/class-plugin.php extending ClassifAI's Feature abstract class:
<?php
namespace MyClassifAIExtension;
use Classifai\Features\Feature;
use Classifai\Providers\OpenAI\ChatGPT;
use Classifai\Services\LanguageProcessing;
class Plugin extends Feature {
/**
* Unique ID for this Feature.
*
* This will be used as:
* - WordPress option name to store settings
* - Feature identifier in ClassifAI's system
*
* @var string
*/
const ID = 'feature_my_custom_feature';
/**
* Constructor - Set up the Feature.
*/
public function __construct() {
// Set the display label for this Feature.
$this->label = __( 'My Custom Feature', 'my-extension' );
// Get all available Providers from the Service.
$this->provider_instances = $this->get_provider_instances(
LanguageProcessing::get_service_providers()
);
// Define which Providers this Feature supports.
$this->supported_providers = [
ChatGPT::ID => __( 'OpenAI ChatGPT', 'my-extension' ),
// Add more Providers as needed
];
}
/**
* Set up hooks that run when the Feature is enabled.
*
* Only fires if the Feature is enabled in settings.
*/
public function feature_setup() {
// Register REST API endpoints.
add_action( 'rest_api_init', [ $this, 'register_endpoints' ] );
// Enqueue editor assets.
add_action( 'enqueue_block_assets', [ $this, 'enqueue_editor_assets' ] );
// Enqueue admin assets (settings page).
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
}
/**
* Get default settings for the Feature.
*
* @return array Default settings.
*/
public function get_feature_default_settings(): array {
return [
'provider' => ChatGPT::ID,
// Add your custom settings here
];
}
/**
* Sanitize settings before saving.
*
* @param array $new_settings Settings being saved.
* @return array Sanitized settings.
*/
public function sanitize_default_feature_settings( array $new_settings ): array {
// Sanitize your custom settings
return $new_settings;
}
/**
* Get description for the enable field.
*
* @return string Feature description.
*/
public function get_enable_description(): string {
return esc_html__( 'Description of what your Feature does.', 'my-extension' );
}
}
Critical Points:
- The
IDconstant must be unique and follow the patternfeature_* feature_setup()only runs when the Feature is enabled- Use
$this->provider_instancesto access configured Providers $this->supported_providerslimits which Providers users can select
Step 4: Register REST API Endpoints
Add REST API functionality to your Feature class:
/**
* Register REST API endpoints.
*/
public function register_endpoints() {
register_rest_route(
'my-extension/v1',
'/process(?:/(?P<id>\d+))?',
[
'methods' => 'POST',
'callback' => [ $this, 'process_request' ],
'permission_callback' => fn() => current_user_can( 'edit_posts' ),
'args' => [
'id' => [
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'is_numeric',
],
],
]
);
}
/**
* Process the API request.
*
* @param \WP_REST_Request $request The REST request.
* @return array|\WP_Error Response or error.
*/
public function process_request( \WP_REST_Request $request ) {
$post_id = $request->get_param( 'id' );
// Validate the Feature is enabled.
if ( ! $this->is_feature_enabled() ) {
return new \WP_Error(
'feature_disabled',
__( 'Feature is not enabled.', 'my-extension' )
);
}
$settings = $this->get_settings();
$provider_id = $settings['provider'] ?? ChatGPT::ID;
// Check Provider authentication.
if ( empty( $settings[ $provider_id ]['authenticated'] ) ) {
return new \WP_Error(
'not_authenticated',
__( 'Provider is not authenticated.', 'my-extension' )
);
}
// Your Feature logic here
$result = $this->do_something( $post_id, $settings );
if ( is_wp_error( $result ) ) {
return $result;
}
return rest_ensure_response( $result );
}
Best Practices:
- Always validate the Feature is enabled
- Check Provider authentication status
- Use
\WP_Errorfor error handling - Return responses with
rest_ensure_response()
Step 5: Integrate with AI Providers
Call AI Provider APIs using ClassifAI's authentication:
/**
* Call the Provider API.
*
* @param string $provider_id Provider ID.
* @param array $settings Feature settings.
* @param string $prompt The prompt to send.
* @param string $content The content to process.
* @return string|\WP_Error Response or error.
*/
private function call_provider_api( string $provider_id, array $settings, string $prompt, string $content ) {
switch ( $provider_id ) {
case ChatGPT::ID:
return $this->call_chatgpt( $settings, $prompt, $content );
// Add more Providers as needed
default:
return new \WP_Error(
'unsupported_provider',
sprintf(
__( 'Provider "%s" is not supported.', 'my-extension' ),
$provider_id
)
);
}
}
/**
* Call ChatGPT API.
*
* @param array $settings Feature settings.
* @param string $prompt System prompt.
* @param string $content Content to process.
* @return string|\WP_Error Response or error.
*/
private function call_chatgpt( array $settings, string $prompt, string $content ) {
$api_key = $settings[ ChatGPT::ID ]['api_key'] ?? '';
if ( empty( $api_key ) ) {
return new \WP_Error(
'missing_api_key',
__( 'API key is missing.', 'my-extension' )
);
}
// Use ClassifAI's APIRequest class for authenticated requests.
$request = new \Classifai\Providers\OpenAI\APIRequest(
$api_key,
$this->get_option_name()
);
$body = [
'model' => 'gpt-4o-mini',
'messages' => [
[
'role' => 'system',
'content' => $prompt,
],
[
'role' => 'user',
'content' => $content,
],
],
'temperature' => 0.3,
'max_tokens' => 2000,
];
// Make the API request.
$response = $request->post(
'https://api.openai.com/v1/chat/completions',
[
'body' => wp_json_encode( $body ),
]
);
if ( is_wp_error( $response ) ) {
return $response;
}
// Extract the response content.
if ( ! empty( $response['choices'][0]['message']['content'] ) ) {
return $response['choices'][0]['message']['content'];
}
return new \WP_Error(
'empty_response',
__( 'Provider returned an empty response.', 'my-extension' )
);
}
Key Points:
- Use
\Classifai\Providers\OpenAI\APIRequestfor OpenAI requests (handles authentication and rate limiting) - Access API keys from
$settings[ $provider_id ]['api_key'] - Each Provider requires its own implementation method
- Apply filters to allow customization of API requests
Step 6: Add Custom Prompts Support
To support custom prompts (like ClassifAI's prompt repeater):
use function Classifai\sanitize_prompts;
use function Classifai\get_default_prompt;
/**
* Default prompt for your Feature.
*
* @var string
*/
public $prompt = 'Your default system prompt here...';
/**
* Get default settings including prompt.
*
* @return array Default settings.
*/
public function get_feature_default_settings(): array {
return [
'my_feature_prompt' => [
[
'title' => __( 'ClassifAI default', 'my-extension' ),
'prompt' => $this->prompt,
'original' => 1,
],
],
'provider' => ChatGPT::ID,
];
}
/**
* Build the prompt for your Feature.
*
* @return string The prompt to use.
*/
private function build_prompt(): string {
$settings = $this->get_settings();
// Get the selected prompt or use default.
$prompt = esc_textarea(
get_default_prompt( $settings['my_feature_prompt'] ?? [] ) ?? $this->prompt
);
/**
* Filter the prompt before sending to provider.
*
* @param string $prompt The prompt.
* @param array $settings Feature settings.
*/
return apply_filters( 'my_extension_prompt', $prompt, $settings );
}
/**
* Sanitize settings including prompts.
*
* @param array $new_settings Settings being saved.
* @return array Sanitized settings.
*/
public function sanitize_default_feature_settings( array $new_settings ): array {
$new_settings['my_feature_prompt'] = sanitize_prompts(
'my_feature_prompt',
$new_settings
);
return $new_settings;
}
/**
* Override get_settings to keep using original prompt.
*
* This ensures the default prompt gets updated when the plugin is updated.
*
* @param string $index Setting index to retrieve.
* @return array|mixed Settings.
*/
public function get_settings( $index = false ) {
$settings = parent::get_settings( $index );
// Update the original prompt from codebase.
if ( $settings && ! empty( $settings['my_feature_prompt'] ) ) {
foreach ( $settings['my_feature_prompt'] as $key => $prompt ) {
if ( 1 === intval( $prompt['original'] ?? 0 ) ) {
$settings['my_feature_prompt'][ $key ]['prompt'] = $this->prompt;
break;
}
}
}
return $settings;
}
Step 7: Create Settings UI
Create package.json for JavaScript dependencies:
{
"name": "my-classifai-extension",
"version": "1.0.0",
"scripts": {
"build": "wp-scripts build src/index.js src/settings.js --output-path=build",
"start": "wp-scripts start src/index.js src/settings.js --output-path=build"
},
"dependencies": {
"@wordpress/api-fetch": "^6.0.0",
"@wordpress/components": "^25.0.0",
"@wordpress/data": "^9.0.0",
"@wordpress/element": "^5.0.0",
"@wordpress/i18n": "^4.0.0",
"@wordpress/plugins": "^6.0.0"
},
"devDependencies": {
"@wordpress/scripts": "^26.0.0"
}
}
Create src/settings.js for the settings interface:
/**
* WordPress dependencies
*/
import { Fill } from '@wordpress/components';
import { registerPlugin } from '@wordpress/plugins';
import { __ } from '@wordpress/i18n';
import { useSelect, useDispatch } from '@wordpress/data';
/**
* Settings component.
*/
const MyFeatureSettings = () => {
// Access Feature settings from ClassifAI's store.
const featureSettings = useSelect( ( select ) =>
select( 'classifai-settings' ).getFeatureSettings()
);
// Get the dispatch function to update settings.
const { setFeatureSettings } = useDispatch( 'classifai-settings' );
return (
<Fill name="ClassifAIFeatureSettings">
{/* Add your custom settings fields here */}
<div className="settings-row">
<div className="settings-label">
{ __( 'My Setting', 'my-extension' ) }
</div>
<div className="settings-control">
{/* Your controls */}
</div>
</div>
</Fill>
);
};
/**
* Register the settings plugin.
*
* IMPORTANT: The scope must match your feature ID with underscores replaced by hyphens.
* Feature ID: feature_my_custom_feature
* Scope: feature-my-custom-feature
*/
registerPlugin( 'feature-my-custom-feature', {
scope: 'feature-my-custom-feature',
render: MyFeatureSettings,
} );
Critical Points:
- Use
Fillcomponent with nameClassifAIFeatureSettings - The
registerPluginscope MUST match your Feature ID (with hyphens instead of underscores) - Access settings via
select( 'classifai-settings' ).getFeatureSettings() - Update settings via
dispatch( 'classifai-settings' ).setFeatureSettings()
Enqueue the settings script in your Feature class:
/**
* Enqueue admin scripts.
*
* @param string $hook_suffix Current admin page.
*/
public function enqueue_admin_assets( string $hook_suffix ) {
// Only on ClassifAI settings page.
if ( 'tools_page_classifai' !== $hook_suffix ) {
return;
}
$asset_file = include( plugin_dir_path( __FILE__ ) . '../build/settings.asset.php' );
wp_enqueue_script(
'my-extension-settings',
plugins_url( '../build/settings.js', __FILE__ ),
$asset_file['dependencies'],
$asset_file['version'],
true
);
}
Step 8: Create Editor Integration
Create src/index.js for block editor integration:
/**
* WordPress dependencies
*/
import { registerPlugin } from '@wordpress/plugins';
import { PluginSidebar, PluginSidebarMoreMenuItem } from '@wordpress/editor';
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { Button, Spinner, Notice } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
/**
* Sidebar component.
*/
const MyFeatureSidebar = () => {
const [ isLoading, setIsLoading ] = useState( false );
const [ results, setResults ] = useState( null );
const [ error, setError ] = useState( null );
// Get current post ID.
const postId = useSelect( ( select ) => {
return select( 'core/editor' ).getCurrentPostId();
}, [] );
/**
* Handle API request.
*/
const handleProcess = async () => {
setIsLoading( true );
setError( null );
setResults( null );
try {
const response = await apiFetch( {
path: `/my-extension/v1/process/${ postId }`,
method: 'POST',
} );
if ( response.code && response.message ) {
setError( response.message );
} else {
setResults( response );
}
} catch ( err ) {
setError(
err.message ||
__( 'An error occurred.', 'my-extension' )
);
} finally {
setIsLoading( false );
}
};
return (
<div style={ { padding: '16px' } }>
<Button
variant="primary"
onClick={ handleProcess }
disabled={ isLoading }
style={ { width: '100%' } }
>
{ isLoading ? (
<>
<Spinner />
{ __( 'Processing...', 'my-extension' ) }
</>
) : (
__( 'Process', 'my-extension' )
) }
</Button>
{ error && (
<Notice status="error" isDismissible={ false }>
{ error }
</Notice>
) }
{ results && (
<div>
{/* Display your results */}
</div>
) }
</div>
);
};
/**
* Register the sidebar plugin.
*/
registerPlugin( 'my-extension', {
render: () => {
return (
<>
<PluginSidebarMoreMenuItem
target="my-extension-sidebar"
icon="admin-generic"
>
{ __( 'My Feature', 'my-extension' ) }
</PluginSidebarMoreMenuItem>
<PluginSidebar
name="my-extension-sidebar"
title={ __( 'My Feature', 'my-extension' ) }
icon="admin-generic"
>
<MyFeatureSidebar />
</PluginSidebar>
</>
);
},
} );
Enqueue the editor script in your Feature class:
/**
* Enqueue editor assets.
*/
public function enqueue_editor_assets() {
global $post;
if ( empty( $post ) ) {
return;
}
$asset_file = include( plugin_dir_path( __FILE__ ) . '../build/index.asset.php' );
wp_enqueue_script(
'my-extension-editor',
plugins_url( '../build/index.js', __FILE__ ),
$asset_file['dependencies'],
$asset_file['version'],
true
);
// Pass data to JavaScript.
wp_localize_script(
'my-extension-editor',
'myExtensionData',
[
'apiUrl' => rest_url( 'my-extension/v1/' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'postId' => $post->ID,
]
);
}
Step 9: Build and Test
Build your JavaScript assets:
# Install dependencies
npm install
# Build for production
npm run build
# Or start development watch mode
npm run start
The build process creates:
build/index.js- Editor integrationbuild/settings.js- Settings page integrationbuild/*.asset.php- Dependency manifests
Step 10: Activate and Configure
-
Activate Your Plugin
- Navigate to WordPress admin → Plugins
- Activate your extension plugin
-
Configure ClassifAI Provider
- Go to Tools → ClassifAI
- Navigate to the appropriate Service (e.g., Language Processing)
- Configure and authenticate your chosen Provider (e.g., ChatGPT)
-
Enable Your Feature
- In the same Service, find your custom Feature
- Enable it and configure settings
- Save changes
-
Test in Editor
- Create or edit a post
- Look for your Feature in the editor toolbar (three dots menu)
- Test the functionality
Advanced Topics
Adding Support for Multiple Providers
To support additional AI Providers:
- Update Constructor:
$this->supported_providers = [
ChatGPT::ID => __( 'OpenAI ChatGPT', 'my-extension' ),
GeminiAPI::ID => __( 'Google AI (Gemini)', 'my-extension' ),
// Add more as needed
];
- Add Provider-Specific Methods:
private function call_provider_api( string $provider_id, array $settings, string $prompt, string $content ) {
switch ( $provider_id ) {
case ChatGPT::ID:
return $this->call_chatgpt( $settings, $prompt, $content );
case GeminiAPI::ID:
return $this->call_gemini( $settings, $prompt, $content );
default:
return new \WP_Error( 'unsupported_provider', __( 'Unsupported provider.', 'my-extension' ) );
}
}
private function call_chatgpt( array $settings, string $prompt, string $content ) {
// ChatGPT-specific implementation
}
private function call_gemini( array $settings, string $prompt, string $content ) {
// Gemini-specific implementation
}
Adding Extensibility Hooks
Provide filters for developers to customize your Feature:
/**
* Filter the prompt.
*
* @param string $prompt The prompt.
* @param array $settings Feature settings.
*/
$prompt = apply_filters( 'my_extension_prompt', $prompt, $settings );
/**
* Filter the API request body.
*
* @param array $body Request body.
* @param array $settings Feature settings.
*/
$body = apply_filters( 'my_extension_request_body', $body, $settings );
/**
* Filter the processed results.
*
* @param array $results Processed results.
* @param int $post_id Post ID.
* @param array $settings Feature settings.
*/
$results = apply_filters( 'my_extension_results', $results, $post_id, $settings );
Custom Settings Components
Create reusable React components for complex settings:
// src/components/CustomControl.js
export const CustomControl = ( { value, onChange } ) => {
return (
<div className="custom-control">
{/* Your control implementation */}
</div>
);
};
// Use in settings.js
import { CustomControl } from './components/CustomControl';
const MyFeatureSettings = () => {
const { featureSettings } = useSelect( /*...*/ );
const { setFeatureSettings } = useDispatch( /*...*/ );
return (
<Fill name="ClassifAIFeatureSettings">
<CustomControl
value={ featureSettings.my_setting }
onChange={ ( newValue ) => {
setFeatureSettings( {
my_setting: newValue,
} );
} }
/>
</Fill>
);
};