WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
55bca24
initialize Azure Language provider
psorensen Aug 1, 2024
d9e08b0
clean up settings and add auth check
psorensen Aug 2, 2024
5369138
add debug info
psorensen Aug 2, 2024
7cde3f9
avoid confusion by disabling feature if not authenticated
psorensen Aug 2, 2024
dbb5135
add excerpt generation
psorensen Aug 2, 2024
f9e56ee
fix filter doc
psorensen Aug 5, 2024
b550389
add helper function to utilize vip remote get
psorensen Aug 6, 2024
db07de9
Merge branch 'develop' into feature/azure-language-service
psorensen Aug 6, 2024
c53e387
update filter docblock
psorensen Sep 6, 2024
19d1774
Make plugin VIP compliant by removing usage of wp_remote_get
sksaju May 12, 2025
128c517
address CR feedback on fix/791-make-plugin-vip-compliant-by-removing-…
sksaju May 19, 2025
21a8431
Merge pull request #901 from 10up/fix/791-make-plugin-vip-compliant-b…
dkotter May 20, 2025
393c74c
Merge branch 'develop' into feature/azure-language-service
psorensen Jun 10, 2025
03468dc
fix typo
psorensen Jun 10, 2025
bac59ea
phpcs
psorensen Jun 10, 2025
428b6d8
update text domain
psorensen Jun 11, 2025
9ad410f
fix undefined warning
psorensen Jun 11, 2025
4f8c2ed
Merge branch 'develop' into feature/azure-language-service
psorensen Jun 25, 2025
c7860f8
update azure endpoint
psorensen Jun 25, 2025
3cf551d
fix: Azure Language auth - wp_remote_get doesn't support body, use wp…
psorensen Jun 25, 2025
13c694b
adds azure language provider settings
psorensen Jun 25, 2025
2542b7f
remove legacy settings
psorensen Jun 25, 2025
a34e4f2
prevent infinite attempts when service never completes
psorensen Jun 30, 2025
656b545
filter max attempts
psorensen Jun 30, 2025
baa47b1
remove test
psorensen Jun 30, 2025
da260d9
feat: add conditional rendering for Azure Language in excerpt generat…
psorensen Jun 30, 2025
f0005cf
add additional error handling
psorensen Jun 30, 2025
0ab81e9
Merge branch 'develop' into feature/azure-language-service
psorensen Jun 30, 2025
4be3fab
phpcs
psorensen Jul 1, 2025
4139791
lint js
psorensen Jul 1, 2025
ffea6de
Fix eslint error
dkotter Jul 1, 2025
25babc8
Merge branch 'develop' into feature/azure-language-service
psorensen Aug 19, 2025
85bfe71
remove duplicate function
Oct 9, 2025
30e5f08
Merge branch 'develop' into feature/azure-language-service
Oct 9, 2025
1cf1029
resolve duplicate use functions
Oct 9, 2025
964f419
phpcs
Oct 9, 2025
0b4655c
restore missing function
Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions includes/Classifai/Features/ExcerptGeneration.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Classifai\Providers\GoogleAI\GeminiAPI;
use Classifai\Providers\OpenAI\ChatGPT;
use Classifai\Providers\Azure\OpenAI;
use Classifai\Providers\Azure\Language;
use Classifai\Providers\Browser\ChromeAI;
use Classifai\Providers\Localhost\Ollama;
use WP_REST_Server;
Expand Down Expand Up @@ -55,6 +56,7 @@ public function __construct() {
ChatGPT::ID => __( 'OpenAI ChatGPT', 'classifai' ),
GeminiAPI::ID => __( 'Google AI (Gemini API)', 'classifai' ),
OpenAI::ID => __( 'Azure OpenAI', 'classifai' ),
Language::ID => __( 'Azure Language', 'classifai' ),
Grok::ID => __( 'xAI Grok', 'classifai' ),
ChromeAI::ID => __( 'Chrome AI (experimental)', 'classifai' ),
Ollama::ID => __( 'Ollama', 'classifai' ),
Expand Down
374 changes: 374 additions & 0 deletions includes/Classifai/Providers/Azure/Language.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,374 @@
<?php
/**
* Azure Language Provider
*
* @package Classifai
*/

namespace Classifai\Providers\Azure;

use Classifai\Providers\Provider;
use Classifai\Features\ExcerptGeneration;
use WP_Error;
use function Classifai\safe_wp_remote_get;


class Language extends Provider {
/**
* The Provider ID.
*
* Required and should be unique.
*/
const ID = 'azure_language';

/**
* The Provider Name.
*
* Required and should be unique.
*/
const API_VERSION = '2023-04-01';

/**
* Analyze Text endpoint.
*
* @var string
*/
const ANALYZE_TEXT_ENDPOINT = '/language/analyze-text/jobs';

/**
* MyProvider constructor.
*
* @param \Classifai\Features\Feature $feature_instance The feature instance.
*/
public function __construct( $feature_instance = null ) {
$this->feature_instance = $feature_instance;
}

/**
* Sanitize the settings for this provider.
*
* Can also be useful to verify the Provider API connection
* works as expected here, returning an error if needed.
*
* @param array $new_settings The settings array.
* @return array
*/
public function sanitize_settings( array $new_settings ): array {
$settings = $this->feature_instance->get_settings();

$new_settings[ static::ID ]['api_key'] = sanitize_text_field( $new_settings[ static::ID ]['api_key'] ?? $settings[ static::ID ]['api_key'] );
$new_settings[ static::ID ]['endpoint_url'] = esc_url_raw( $new_settings[ static::ID ]['endpoint_url'] ?? $settings[ static::ID ]['endpoint_url'] );
$new_settings[ static::ID ]['authenticated'] = false;

if ( ! empty( $new_settings[ static::ID ]['endpoint_url'] ) && ! empty( $new_settings[ static::ID ]['api_key'] ) ) {
$new_settings[ static::ID ]['authenticated'] = $this->authenticate_credentials(
$new_settings[ static::ID ]['endpoint_url'],
$new_settings[ static::ID ]['api_key']
);
}

if ( ! $new_settings[ static::ID ]['authenticated'] ) {
add_settings_error(
'authenticated',
400,
esc_html( 'There was an error authenticating with Azure Language Services. Please check your credentials.' ),
'error'
);

// disable the feature if we're unable to authenticate./
$new_settings['status'] = 0;
}

return $new_settings;
}

/**
* Authenticates our credentials.
*
* Performs a simple test to ensure the credentials are valid.
*
* @param string $url Endpoint URL.
* @param string $api_key Api Key.
*
* @return bool|WP_Error
*/
protected function authenticate_credentials( string $url, string $api_key ) {
$rtn = false;

$endpoint = trailingslashit( $url ) . 'language/:analyze-text';
$endpoint = add_query_arg( 'api-version', static::API_VERSION, $endpoint );

$body = [
'kind' => 'LanguageDetection',
'parameters' => [
'modelVersion' => 'latest',
],
'analysisInput' => [
'documents' => [
[
'id' => '1',
'text' => 'Hello world',
],
],
],
];

$request = wp_remote_post(
$endpoint,
[
'headers' => [
'Ocp-Apim-Subscription-Key' => $api_key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( $body ),
]
);

if ( ! is_wp_error( $request ) ) {
$response = json_decode( wp_remote_retrieve_body( $request ) );
if ( ! empty( $response->error ) ) {
$rtn = new WP_Error( 'auth', $response->error->message );
} else {
$rtn = true;
}
}

return $rtn;
}

/**
* Common entry point for all REST endpoints for this provider.
*
* All Features will end up calling the rest_endpoint_callback method for their assigned Provider.
* This method should validate the route that is being called and then call the appropriate method
* for that route. This method typically will validate we have all the required data and if so,
* make a request to the appropriate API endpoint.
*
* @param int $post_id The Post ID we're processing.
* @param string $route_to_call The route we are processing.
* @param array $args Optional arguments to pass to the route.
* @return string|WP_Error
*/
public function rest_endpoint_callback( $post_id = 0, string $route_to_call = '', array $args = [] ) {
if ( ! $post_id || ! get_post( $post_id ) ) {
return new WP_Error( 'post_id_required', esc_html__( 'A valid post ID is required to generate an excerpt.', 'classifai' ) );
}

$route_to_call = strtolower( $route_to_call );
$return = '';

switch ( $route_to_call ) {
case 'excerpt':
$return = $this->generate_excerpt( $post_id, $args );
break;
}

return $return;
}

/**
* Generate an excerpt for a given post.
*
* This service requires two API calls - one to request the summary and another to retrieve the summary.
*
* @param int $post_id The Post ID we're processing.
* @param array $args Optional arguments to pass to the route.
* @return string
*/
public function generate_excerpt( $post_id, $args ) {
$feature = new ExcerptGeneration();
$settings = $feature->get_settings();
$api_key = $settings[ static::ID ]['api_key'];
$endpoint_url = $settings[ static::ID ]['endpoint_url'];
$post_content = get_post_field( 'post_content', $post_id );

// Request the summary form the API.
$summary_request_url = $this->request_summary( $endpoint_url, $api_key, $post_content, $post_id );
if ( is_wp_error( $summary_request_url ) ) {
return $summary_request_url;
}

// Retrieve the Summary after the job is complete.
$summary = $this->retrieve_summary( $summary_request_url );

return $summary;
}

/**
* Request the summary from the API.
*
* @param string $endpoint_url The endpoint URL.
* @param string $api_key The API key.
* @param string $post_content The post content.
* @param int $post_id The post ID.
*
* @return mixed The summary job URL or WP_Error.
*/
public function request_summary( $endpoint_url, $api_key, $post_content, $post_id ) {
$endpoint_url = add_query_arg(
'api-version',
static::API_VERSION,
$endpoint_url . static::ANALYZE_TEXT_ENDPOINT
);

$body = [
'analysisInput' => [
'documents' => [
[
'id' => '1',
'language' => 'en',
'text' => $post_content,
],
],
],
'tasks' => [
[
'kind' => 'AbstractiveSummarization',
'taskName' => 'Classifai Summarization ' . $post_id,
'parameters' => [
/**
* Filter the summary length.
* Possible values are 'oneSentence', 'short', 'medium', 'long'.
* Default is 'oneSentence'.
*
* @since 3.2.0
* @hook classifai_azure_language_summary_length
* @param {string} $summary_length The summary length.
* @return {string} The summary length
*/
'summaryLength' => apply_filters( 'classifai_azure_language_summary_length', 'oneSentence' ),
],
],
],
];

$request = wp_remote_post(
$endpoint_url,
[
'headers' => [
'Ocp-Apim-Subscription-Key' => $api_key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( $body ),
]
);

$headers = wp_remote_retrieve_headers( $request );

if ( ! is_wp_error( $request ) ) {
$response = json_decode( wp_remote_retrieve_body( $request ) );
if ( ! empty( $response->error ) ) {
return new WP_Error( 'summary_request', $response->error->message );
} elseif ( empty( $headers['operation-location'] ) ) {
return new WP_Error( 'summary_request', esc_html__( 'There was an error requesting the summary.', 'classifai' ) );
} else {
return $headers['operation-location'];
}
}

return $request;
}

/**
* Get the text analysis job response
*
* The endpoint URL is returned in the 'operation-location' header of the initial request.
* The job response will return a 'status' property if the job is completed.
* If the job is not completed, wait a second and try again.
*
* @param string $url The URL to analyze.
* @see https://learn.microsoft.com/en-us/azure/ai-services/language-service/summarization/quickstart?tabs=text-summarization%2Cmacos&pivots=rest-api
* @return mixed
*/
private function retrieve_summary( $url ) {
$api_key = $this->feature_instance->get_settings( static::ID )['api_key'];

$request = safe_wp_remote_get(
$url,
[
'headers' => [
'Ocp-Apim-Subscription-Key' => $api_key,
'Content-Type' => 'application/json',
],
]
);

$summary = '';

if ( ! is_wp_error( $request ) ) {
$response = json_decode( wp_remote_retrieve_body( $request ) );
if ( ! empty( $response->error ) ) {
return new WP_Error( 'auth', $response->error->message );
}

$attempts = 0;
/**
* Filter the maximum number of attempts to retrieve the summary.
* Increment this value if you are experiencing timeout errors.
*
* @since 3.2.0
* @hook classifai_azure_language_max_attempts
* @param {int} $max_attempts The maximum number of attempts.
* @return {int} The maximum number of attempts.
*/
$max_attempts = apply_filters( 'classifai_azure_language_max_attempts', 10 );

while ( 'succeeded' !== $response->status && $attempts < $max_attempts ) {
++$attempts;
sleep( 2 );
$request = safe_wp_remote_get(
$url,
[
'headers' => [
'Ocp-Apim-Subscription-Key' => $api_key,
'Content-Type' => 'application/json',
],
]
);
$response = json_decode( wp_remote_retrieve_body( $request ) );
}

// Check if we exceeded max attempts
if ( $attempts >= $max_attempts && 'running' === $response->status ) {
return new WP_Error( 'timeout', esc_html__( 'Summary retrieval is still running after 10 attempts. Please retry.', 'classifai' ) );
}

$summary = ! empty( $response->tasks->items[0]->results->documents[0]->summaries[0] )
? $response->tasks->items[0]->results->documents[0]->summaries[0]->text
: '';

if ( empty( $summary ) ) {
return new WP_Error( 'no_results', esc_html__( 'No summary was generated. Please save the post and retry.', 'classifai' ) );
}
}

return $summary;
}

/**
* Returns the debug information for the provider settings.
*
* This is used to display various settings in the Site Health screen.
* Not required but useful for debugging.
*
* @return array
*/
public function get_debug_information(): array {
$settings = $this->feature_instance->get_settings();
$provider_settings = $settings[ static::ID ];
$debug_info = [];

if ( $this->feature_instance instanceof ExcerptGeneration ) {
$debug_info[ __( 'Excerpt length', 'classifai' ) ] = apply_filters( 'classifai_azure_language_summary_length', 'oneSentence' );
$debug_info[ __( 'Provider', 'classifai' ) ] = 'Azure Language Services';
$debug_info[ __( 'Endpoint URL', 'classifai' ) ] = $provider_settings['endpoint_url'];
}

return apply_filters(
'classifai_' . self::ID . '_debug_information',
$debug_info,
$settings,
$this->feature_instance
);
}
}
Loading