Skip to content
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

Feature-first refactor #611

Merged
merged 164 commits into from
Jan 31, 2024
Merged

Feature-first refactor #611

merged 164 commits into from
Jan 31, 2024

Conversation

Sidsector9
Copy link
Member

@Sidsector9 Sidsector9 commented Nov 6, 2023

Description of the Change

Closes #

Before reading further, it would be good to have this branch opened in the code-editor for reference.
For clarity, the TitleGeneration.php and ExcerptGeneration.php class can be referred.
The classes and methods are internally documented which can be handy.

Add new feature

Internal developers

All the features live inside the includes/Classifai/Features directory and every feature extends from the Feature class.

3rd party developers

<?php

use Classifai\Features\TitleGeneration;
use Classifai\Features\ExcerptGeneration;
use \Classifai\Providers\Provider;
use \Classifai\Features\Feature;

/**
 * This is a custom provider that demonstrates how a custom provider can be added to ClassifAI.
 * The following implementation shows how it can be used to generate titles.
 */
class TenupAI extends Provider {

    /**
     * ID of the provider.
     *
     * - This is mandatory.
     * - This should be unique.
     */
    const ID = 'tenupai';

    public function __construct( $feature_instance = null ) {
        parent::__construct(
            'TenupAI Text',
            'TenupAI',
            self::ID
        );

        /**
         * Every provider must set the `feature_instance` variable with the feature instance.
         * The feature which calls this provider will pass itself as an argument.
         */
        $this->feature_instance = $feature_instance;
    }

    /**
     * Ignore this method. Will be removed in near future.
     */
    public function register() {}

    /**
     * This method will be called by the feature to render the fields
     * required by the provider, such as API key, endpoint URL, etc.
     *
     * This should also register settings that are required by the title
     * generation feature to work. For example, if the provider can return multiple
     * titles in a single request, then a setting to control the number of titles should
     * be registered here.
     */
    public function render_provider_fields() {
        $settings = $this->feature_instance->get_settings( TenupAI::ID );

        /**
         * These are the common fields that will be necessary for every
         * feature that uses this provider.
         */
        add_settings_field(
            static::ID . '_api_key',
            esc_html__( 'API Key', 'classifai' ),
            [ $this->feature_instance, 'render_input' ],
            $this->feature_instance->get_option_name(),
            $this->feature_instance->get_option_name() . '_section',
            [
                'option_index'  => TenupAI::ID,
                'label_for'     => 'api_key',
                'input_type'    => 'password',
                'default_value' => $settings['api_key'],
                'class'         => 'classifai-provider-field hidden' . ' provider-scope-' . TenupAI::ID, // Important to add this.
            ]
        );

        switch ( $this->feature_instance::ID ) {
            case TitleGeneration::ID:
                // add_settings_field() for settings required by the title generation feature.
                // Example would be number of titles to generate.
                break;

            case ExcerptGeneration::ID:
                // add_settings_field() for settings required by the excerpt generation feature.
                // Example would be the sentiment of the excerpt.
                break;
        }
    }

    public function get_default_provider_settings() {
        /**
         * Default common settings required by any feature that uses this provider.
         */
        $common_settings = [
            'api_key'       => '',
            'authenticated' => false,
        ];

        /**
         * Default values for feature specific settings.
         */
        switch ( $this->feature_instance::ID ) {
            case TitleGeneration::ID:
                return array_merge(
                    $common_settings,
                    [
                        'number_of_titles' => 1,
                    ]
                );

            case ExcerptGeneration::ID:
                return array_merge(
                    $common_settings,
                    [
                        'sentiment' => 'neutral',
                    ]
                );
        }

        return $common_settings;
    }

    /**
     * Feature specific settings (settings that don't depend on any provider)
     * are sanitized within the feature class itself.
     *
     * This method is used to sanitize provider specific settings.
     *
     * You shoud connect to the provider's API here and verify the API key if necessary.
     *
     * @param array $new_settings Incoming settings that the user is trying to save.
     */
    public function sanitize_settings( $new_settings ) {
        $settings = $this->feature_instance->get_settings();

        if ( $this->feature_instance instanceof TitleGeneration ) {
            $new_settings[ TenupAI::ID ]['api_key']       = sanitize_text_field( $new_settings[ TenupAI::ID ]['api_key'] ?? $settings[ TenupAI::ID ]['api_key'] );
            $new_settings[ TenupAI::ID ]['authenticated'] = true; // Hardcoding it for this example.
        }

        return $new_settings;
    }

    /**
     * Our mock function that generates a title using the TenupAI API provider.
     */
    public function generate_title() {
        return 'This is a title generated by TenupAI.';
    }
}

/* ==================================================== *
 * END OF PROVIDER CLASS
/* ==================================================== */


/**
 * Register the provider with ClassifAI.
 */
add_filter( 'classifai_language_processing_service_providers', function( $providers ) {
    $providers[] = TenupAI::class;
    return $providers;
} );

/**
 * Add the TenupAI provider to the Title generation feature's
 * provider selection dropdown.
 */
add_filter( 'classifai_' . TitleGeneration::ID . '_providers', function( $providers ) {
    $providers[ TenupAI::ID ] = __( 'TenupAI', 'classifai' );
    return $providers;
} );

/**
 * Filter the return value of the Title Generation's run() method with
 * the result from the TenupAI provider for title generation.
 *
 * @param string   $result The title generated by the provider.
 * @param Provider $provider_instance The provider instance.
 * @param array    $args Arguments passed to the run() method.
 * @param Feature   $feature The feature instance.
 */
add_filter( 'classifai_feature_title_generation_run', function( $result, $provider_instance, $args, $feature ) {
    if ( TenupAI::ID !== $provider_instance::ID ) {
        return $result;
    }

    /** @var TenupAI $provider_instance */
    $result = call_user_func_array(
        [ $provider_instance, 'generate_title' ],
        [ ...$args ]
    );

    return $result;
}, 10, 4 );

if ( class_exists( '\WP_CLI' ) ) {
    /**
     * Example command that demonstrates how a custom provider
     * can be used to generate a title.
     *
     * <post-id>
     * : An awesome message to display
     *
     * @when before_wp_load
     */
    $foo = function( $args, $assoc_args ) {
        // Add your steps for checking for post ID.

        // Run the title generation feature.
        $feature = new TitleGeneration();
        $result = $feature->run( 12 );

        WP_CLI::success( $result );
    };
    WP_CLI::add_command( 'tenup_generate_title', $foo );
}

wp_options data

Each Feature class must define a const ID that is a unique identifier for the feature.
With the feature-first approach, every feature stores its data in a separate row in the wp_options table.
For example, the ID for Title and Excerpt generation are feature_title_generation and feature_excerpt_generation, so the corresponding data in the options table will have keys classifai_feature_title_generation and classifai_feature_excerpt_generation.

Feature setting data format:

Array
(
    [status] => 1
    [roles] => Array
        (
            [administrator] => administrator
            [editor] => 0
            [author] => 0
            [contributor] => 0
            [subscriber] => 0
        )

    [length] => 100
    [provider] => openai_chatgpt
    [openai_chatgpt] => Array
        (
            [api_key] => 
            [number_of_titles] => 4
            [authenticated] => 1
            [generate_title_prompt] => Array
                (
                    [0] => Array
                        (
                            [title] => ClassifAI default
                            [prompt] => Write an SEO-friendly title for the following content that will encourage readers to clickthrough, staying within a range of 40 to 60 characters.
                            [default] => 1
                            [original] => 1
                        )

                )

        )

)

All the root level keys (except openai_chatgpt) are feature-level settings, and all the data against the openai_chatgpt key are the provider-level settings.

A Provider class has access to all the settings of a feature as the provider is tied to the feature via its constructor.

Important areas to understand:

  • Feature registration under a service.
  • Adding feature and provider-level setting fields (see the public function setup_fields_sections() in the method in the TitleGeneration.phpandChatGPT.php` files.
  • Sanitization of feature and provider-level settings (see the public function sanitize_settings( $settings ) method in the TitleGeneration.php file.
  • Observe the changes that were necessary in the LanguageProcessing.php to accommodate the feature-first approach.

Checklist

  • Migrate Title generation under Language Processing
  • Migrate Excerpt generation under Language Processing
  • Migrate Content Resizing under Language Processing
  • Migrate Text to Speech under Language Processing
  • Move logic out of the SavePostHandler.php to respective Provider classes. (see the migration of the synthesize_speech from the SavePostHandler.php file to Speech.php file. Similar changes have to be done for other features that run when a post is published/updated.
  • Figure out how split existing Image Processing services into features (Tagging, Descriptive text, Smart cropping, PDF scanning, etc)
  • Move Audio transcripts generation as a separate feature under Language Processing.
  • Show/hide the provider-level fields depending on the selected provider. (As most of the language processing features had ChatGPT as the provider, I did not get the chance to implement the show/hide functionality.)
  • Verify WP-CLI support works as expected, add changes if necessary.
  • Verify bulk/quick action works as expected, add changes if necessary.

Changelog Entry

Changed - Refactor admin screen to feature first settings

Credits

Props @dkotter @Sidsector9

Checklist:

  • I agree to follow this project's Code of Conduct.
  • I have updated the documentation accordingly.
  • I have added tests to cover my change.
  • All new and existing tests pass.

@Sidsector9 Sidsector9 requested review from dkotter, jeffpaul and a team as code owners November 6, 2023 18:25
@jeffpaul jeffpaul added this to the 2.4.0 milestone Nov 6, 2023
@Sidsector9 Sidsector9 changed the title upkeep: Feature-first refactor of admin screen [WIP]: upkeep: Feature-first refactor of admin screen Nov 6, 2023
@dkotter dkotter modified the milestones: 2.4.0, 3.0.0 Nov 7, 2023
[WIP]: Updated E2E tests for feature-first refactor
@dkotter dkotter changed the title upkeep: Feature-first refactor of admin screen Feature-first refactor Jan 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Multiple image processing features don't work together
5 participants