PluginProbe
AI / 0.2.0
AI v0.2.0
1.3.0 1.2.0 1.1.0 1.0.2 1.0.1 1.0.0 0.9.0 trunk 0.1.1 0.2.0 0.2.1 0.3.0 0.3.1 0.4.0 0.4.1 0.5.0 0.6.0 0.7.0 0.8.0
ai / includes / Abilities / Summarization / Summarization.php

Summarization.php in AI 0.2.0, at includes/Abilities/Summarization/Summarization.php

253 lines 6.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Content summarization WordPress Ability implementation.
4 *
5 * @package WordPress\AI
6 */
7
8 declare( strict_types=1 );
9
10 namespace WordPress\AI\Abilities\Summarization;
11
12 use WP_Error;
13 use WordPress\AI\Abstracts\Abstract_Ability;
14 use WordPress\AI_Client\AI_Client;
15
16 use function WordPress\AI\get_post_context;
17 use function WordPress\AI\get_preferred_models;
18 use function WordPress\AI\normalize_content;
19
20 /**
21 * Content summarization WordPress Ability.
22 *
23 * @since 0.2.0
24 */
25 class Summarization extends Abstract_Ability {
26
27 /**
28 * The default length of the summary.
29 *
30 * @since 0.2.0
31 *
32 * @var string
33 */
34 protected const LENGTH_DEFAULT = 'medium';
35
36 /**
37 * {@inheritDoc}
38 *
39 * @since 0.2.0
40 */
41 protected function input_schema(): array {
42 return array(
43 'type' => 'object',
44 'properties' => array(
45 'content' => array(
46 'type' => 'string',
47 'sanitize_callback' => 'sanitize_text_field',
48 'description' => esc_html__( 'Content to summarize.', 'ai' ),
49 ),
50 'context' => array(
51 'type' => 'string',
52 'sanitize_callback' => 'sanitize_text_field',
53 'description' => esc_html__( 'Additional context to use when summarizing the content. This can either be a string of additional context or can be a post ID that will then be used to get context from that post (if it exists). If no content is provided but a valid post ID is used here, the content from that post will be used.', 'ai' ),
54 ),
55 'length' => array(
56 'type' => 'enum',
57 'enum' => array( 'short', 'medium', 'long' ),
58 'default' => self::LENGTH_DEFAULT,
59 'description' => esc_html__( 'The length of the summary.', 'ai' ),
60 ),
61 ),
62 );
63 }
64
65 /**
66 * {@inheritDoc}
67 *
68 * @since 0.2.0
69 */
70 protected function output_schema(): array {
71 return array(
72 'type' => 'string',
73 'description' => esc_html__( 'The summary of the content.', 'ai' ),
74 );
75 }
76
77 /**
78 * {@inheritDoc}
79 *
80 * @since 0.2.0
81 */
82 protected function execute_callback( $input ) {
83 // Default arguments.
84 $args = wp_parse_args(
85 $input,
86 array(
87 'content' => null,
88 'context' => null,
89 'length' => self::LENGTH_DEFAULT,
90 ),
91 );
92
93 // If a post ID is provided, ensure the post exists before using its' content.
94 if ( is_numeric( $args['context'] ) ) {
95 $post = get_post( (int) $args['context'] );
96
97 if ( ! $post ) {
98 return new WP_Error(
99 'post_not_found',
100 /* translators: %d: Post ID. */
101 sprintf( esc_html__( 'Post with ID %d not found.', 'ai' ), absint( $args['context'] ) )
102 );
103 }
104
105 // Get the post context.
106 $context = get_post_context( $post->ID );
107 $content = $context['content'] ?? '';
108 unset( $context['content'] );
109
110 // Default to the passed in content if it exists.
111 if ( $args['content'] ) {
112 $content = normalize_content( $args['content'] );
113 }
114 } else {
115 $content = normalize_content( $args['content'] ?? '' );
116 $context = $args['context'] ?? '';
117 }
118
119 // If we have no content, return an error.
120 if ( empty( $content ) ) {
121 return new WP_Error(
122 'content_not_provided',
123 esc_html__( 'Content is required to generate a summary.', 'ai' )
124 );
125 }
126
127 // Generate the summary.
128 $result = $this->generate_summary( $content, $context, $args['length'] );
129
130 // If we have an error, return it.
131 if ( is_wp_error( $result ) ) {
132 return $result;
133 }
134
135 // If we have no results, return an error.
136 if ( empty( $result ) ) {
137 return new WP_Error(
138 'no_results',
139 esc_html__( 'No summary was generated.', 'ai' )
140 );
141 }
142
143 // Return the summary in the format the Ability expects.
144 return sanitize_text_field( trim( $result ) );
145 }
146
147 /**
148 * {@inheritDoc}
149 *
150 * @since 0.2.0
151 */
152 protected function permission_callback( $args ) {
153 $post_id = isset( $args['context'] ) && is_numeric( $args['context'] ) ? absint( $args['context'] ) : null;
154
155 if ( $post_id ) {
156 $post = get_post( $post_id );
157
158 // Ensure the post exists.
159 if ( ! $post ) {
160 return new WP_Error(
161 'post_not_found',
162 /* translators: %d: Post ID. */
163 sprintf( esc_html__( 'Post with ID %d not found.', 'ai' ), absint( $post_id ) )
164 );
165 }
166
167 // Ensure the user has permission to read this particular post.
168 if ( ! current_user_can( 'read_post', $post_id ) ) {
169 return new WP_Error(
170 'insufficient_capabilities',
171 esc_html__( 'You do not have permission to summarize this post.', 'ai' )
172 );
173 }
174
175 // Ensure the post type is allowed in REST endpoints.
176 $post_type = get_post_type( $post_id );
177
178 if ( ! $post_type ) {
179 return false;
180 }
181
182 $post_type_obj = get_post_type_object( $post_type );
183
184 if ( ! $post_type_obj || empty( $post_type_obj->show_in_rest ) ) {
185 return false;
186 }
187 } elseif ( ! current_user_can( 'edit_posts' ) ) {
188 // Ensure the user has permission to edit posts in general.
189 return new WP_Error(
190 'insufficient_capabilities',
191 esc_html__( 'You do not have permission to summarize content.', 'ai' )
192 );
193 }
194
195 return true;
196 }
197
198 /**
199 * {@inheritDoc}
200 *
201 * @since 0.2.0
202 */
203 protected function meta(): array {
204 return array(
205 'show_in_rest' => true,
206 );
207 }
208
209 /**
210 * Generates a summary from the given content.
211 *
212 * @since 0.2.0
213 *
214 * @param string $content The content to summarize.
215 * @param string|array<string, string> $context Additional context to use.
216 * @param string $length The desired length of the summary.
217 * @return string|\WP_Error The generated summary, or a WP_Error if there was an error.
218 */
219 protected function generate_summary( string $content, $context, string $length ) {
220 // Convert the context to a string if it's an array.
221 if ( is_array( $context ) ) {
222 $context = implode(
223 "\n",
224 array_map(
225 static function ( $key, $value ) {
226 return sprintf(
227 '%s: %s',
228 ucwords( str_replace( '_', ' ', $key ) ),
229 $value
230 );
231 },
232 array_keys( $context ),
233 $context
234 )
235 );
236 }
237
238 $content = '<content>' . $content . '</content>';
239
240 // If we have additional context, add it to the content.
241 if ( $context ) {
242 $content .= "\n\n<additional-context>" . $context . '</additional-context>';
243 }
244
245 // Generate the summary using the AI client.
246 return AI_Client::prompt_with_wp_error( $content )
247 ->using_system_instruction( $this->get_system_instruction( 'system-instruction.php', array( 'length' => $length ) ) )
248 ->using_temperature( 0.9 )
249 ->using_model_preference( ...get_preferred_models() )
250 ->generate_text();
251 }
252 }
253