PluginProbe
WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards / 5.5.82
WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards v5.5.82
5.5.83 5.5.82 5.5.81 5.5.80 5.5.79 5.5.77 5.5.76 5.5.75 5.5.73 5.5.72 5.5.22 5.5.23 5.5.29 5.5.3 5.5.31 5.5.32 5.5.34 5.5.35 5.5.36 5.5.37 5.5.4 5.5.40 5.5.41 5.5.42 5.5.43 All 159 releases
wp-data-access / WPDataAccess / API / WPDA_AI.php

WPDA_AI.php in WP Data Access – App Builder for Tables, Forms, Charts, Maps & Dashboards 5.5.82, at WPDataAccess/API/WPDA_AI.php

277 lines 10.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDataAccess\API;
4
5 use WPDataAccess\Connection\WPDADB;
6 use WPDataAccess\Data_Dictionary\WPDA_Dictionary_Lists;
7 use WPDataAccess\WPDA;
8 class WPDA_AI extends WPDA_API_Core {
9 const CIPHER = 'AES-256-CBC';
10
11 const AI_API_KEY = 'wpda_ai_key';
12
13 const SUPPORTED_MODELS = ['gpt-3.5-turbo', 'gpt-4-turbo'];
14
15 public function register_rest_routes() {
16 register_rest_route( WPDA_API::WPDA_NAMESPACE, 'ai/sql', array(
17 'methods' => array('POST'),
18 'callback' => array($this, 'ai_sql'),
19 'permission_callback' => function () {
20 return $this->current_user_can_access();
21 },
22 'args' => array(
23 'prompt' => array(
24 'required' => true,
25 'type' => 'string',
26 'description' => __( 'Prompt', 'wp-data-access' ),
27 'sanitize_callback' => 'sanitize_text_field',
28 'validate_callback' => 'rest_validate_request_arg',
29 ),
30 'model' => array(
31 'required' => true,
32 'type' => 'string',
33 'description' => __( 'Model', 'wp-data-access' ),
34 'sanitize_callback' => 'sanitize_text_field',
35 'validate_callback' => function ( $param ) {
36 return in_array( $param, self::SUPPORTED_MODELS );
37 },
38 ),
39 'explain' => array(
40 'required' => true,
41 'type' => 'boolean',
42 'description' => __( 'Add explanations', 'wp-data-access' ),
43 'sanitize_callback' => 'sanitize_text_field',
44 'validate_callback' => 'rest_validate_request_arg',
45 ),
46 ),
47 ) );
48 register_rest_route( WPDA_API::WPDA_NAMESPACE, 'ai/hints', array(
49 'methods' => array('POST'),
50 'callback' => array($this, 'hints'),
51 'permission_callback' => '__return_true',
52 'args' => array(
53 'dbs' => $this->get_param( 'dbs' ),
54 ),
55 ) );
56 register_rest_route( WPDA_API::WPDA_NAMESPACE, 'ai/enabled', array(
57 'methods' => array('POST'),
58 'callback' => array($this, 'enabled'),
59 'permission_callback' => '__return_true',
60 'args' => array(),
61 ) );
62 register_rest_route( WPDA_API::WPDA_NAMESPACE, 'ai/enable', array(
63 'methods' => array('POST'),
64 'callback' => array($this, 'enable'),
65 'permission_callback' => '__return_true',
66 'args' => array(
67 'key' => array(
68 'required' => true,
69 'type' => 'string',
70 'description' => __( 'API Key', 'wp-data-access' ),
71 'sanitize_callback' => 'sanitize_text_field',
72 'validate_callback' => 'rest_validate_request_arg',
73 ),
74 'encrypt' => array(
75 'required' => true,
76 'type' => 'boolean',
77 'description' => __( 'Encrypt API key', 'wp-data-access' ),
78 'sanitize_callback' => 'sanitize_text_field',
79 'validate_callback' => 'rest_validate_request_arg',
80 ),
81 ),
82 ) );
83 }
84
85 public function ai_sql( $request ) {
86 $timeout = 30;
87 $prompt = $request['prompt'];
88 $model = $request['model'];
89 $explain = ( $request['explain'] ? 'Write the SQL query first in a code block. After the code block, provide a clear, concise explanation of what the query does.' : 'Provide only the query without further explanation.' );
90 $prompt = "\nYou are a professional MySQL consultant helping developers write SQL queries.\nAlways respond with clean, optimized MySQL code.\nPlace the query inside a single Markdown code block using triple backticks (```sql).\nDo not include a semicolon at the end of the query.\nImportant: Do not include a LIMIT clause unless the user specifically requests limiting the number of results.\nAssume the system will handle limits automatically if needed.\n{$explain}\nExample input for a user:\nWrite a join between tables dept and emp and show the average and total salaries per department.\nExpected Output:\n```sql\nSELECT d.dname AS department_name, \n AVG(e.sal) AS average_salary, \n SUM(e.sal) AS total_salary\nFROM dept d\nJOIN emp e ON d.deptno = e.deptno\nGROUP BY d.dname\n```\n{$prompt}\n";
91 WPDA::wpda_log_wp_error( $prompt );
92 $api_key_saved = get_user_meta( get_current_user_id(), self::AI_API_KEY, true );
93 if ( false === $api_key_saved || '' === $api_key_saved ) {
94 $api_key = '';
95 } else {
96 $api_key = substr( $api_key_saved, 0, -2 );
97 $is_encrypted = substr( $api_key_saved, -1 );
98 if ( '1' === $is_encrypted ) {
99 // Decrypt API key
100 $api_key = $this->decrypt( $api_key );
101 }
102 }
103 if ( '' === trim( $api_key ) ) {
104 return new \WP_Error('error', 'Invalid or missing API Key', array(
105 'status' => 403,
106 ));
107 }
108 return $this->ask_ai(
109 $api_key,
110 $model,
111 $prompt,
112 $timeout
113 );
114 }
115
116 private function ask_ai(
117 $api_key,
118 $model,
119 $prompt,
120 $timeout,
121 $msg = ''
122 ) {
123 $response = wp_remote_post( 'https://api.openai.com/v1/chat/completions', array(
124 'headers' => array(
125 'Authorization' => 'Bearer ' . $api_key,
126 'Content-Type' => 'application/json',
127 ),
128 'body' => json_encode( array(
129 'model' => $model,
130 'messages' => array(array(
131 'role' => 'user',
132 'content' => $prompt,
133 )),
134 ) ),
135 'timeout' => $timeout,
136 ) );
137 if ( !is_wp_error( $response ) ) {
138 $body = json_decode( wp_remote_retrieve_body( $response ), true );
139 if ( '' !== $msg ) {
140 $body['msg'] = $msg;
141 }
142 return rest_ensure_response( $body );
143 }
144 if ( self::SUPPORTED_MODELS[1] === $model ) {
145 // Try 'gpt-3.5-turbo' if 'gpt-4-turbo' failed
146 return $this->ask_ai(
147 $api_key,
148 self::SUPPORTED_MODELS[0],
149 $prompt,
150 $timeout,
151 'Note: This result was generated using gpt-3.5-turbo due to a timeout using gpt-4-turbo.'
152 );
153 }
154 return new \WP_Error('error', $response->get_error_message(), array(
155 'status' => 403,
156 ));
157 }
158
159 public function hints( $request ) {
160 if ( !$this->current_user_can_access() ) {
161 return $this->unauthorized();
162 }
163 if ( !$this->current_user_token_valid( $request ) ) {
164 return $this->invalid_nonce();
165 }
166 $dbs = $request->get_param( 'dbs' );
167 $tables = WPDA_Dictionary_Lists::get_tables( true, $dbs );
168 $wpdadb = WPDADB::get_db_connection( $dbs );
169 if ( null === $wpdadb ) {
170 // Error connecting.
171 return new \WP_Error('error', "Error connecting to database {$dbs}", array(
172 'status' => 420,
173 ));
174 }
175 $hints = array();
176 foreach ( $tables as $table ) {
177 $table_name = WPDA::remove_backticks( $table['table_name'] );
178 $sql_cmd = $wpdadb->get_results( "SHOW CREATE TABLE `{$table_name}`", 'ARRAY_N' );
179 if ( '' === $wpdadb->last_error && isset( $sql_cmd[0][1] ) ) {
180 $hints[$table_name] = $sql_cmd[0][1];
181 }
182 }
183 return $this->WPDA_Rest_Response( '', $hints );
184 }
185
186 public function enabled( $request ) {
187 if ( !$this->current_user_can_access() ) {
188 return $this->unauthorized();
189 }
190 if ( !$this->current_user_token_valid( $request ) ) {
191 return $this->invalid_nonce();
192 }
193 $api_key = get_user_meta( WPDA::get_current_user_id(), self::AI_API_KEY, true );
194 $is_enabled = false !== $api_key && '' !== $api_key;
195 return $this->WPDA_Rest_Response( '', array(
196 'enabled' => $is_enabled,
197 'encryption' => $this->get_encryption_key() !== null,
198 ) );
199 }
200
201 public function enable( $request ) {
202 if ( !$this->current_user_can_access() ) {
203 return $this->unauthorized();
204 }
205 if ( !$this->current_user_token_valid( $request ) ) {
206 return $this->invalid_nonce();
207 }
208 $key = $request['key'];
209 $encrypt = ( $request['encrypt'] ? 1 : 0 );
210 if ( $encrypt ) {
211 $key = $this->encrypt( $key );
212 }
213 update_user_meta( WPDA::get_current_user_id(), self::AI_API_KEY, "{$key}|{$encrypt}" );
214 return $this->WPDA_Rest_Response( '' );
215 }
216
217 private function encrypt( $string ) {
218 $key = $this->get_encryption_key();
219 if ( null === $key ) {
220 return $string;
221 }
222 $ivlen = openssl_cipher_iv_length( self::CIPHER );
223 $iv = openssl_random_pseudo_bytes( $ivlen );
224 $ciphertext_raw = openssl_encrypt(
225 $string,
226 self::CIPHER,
227 $key,
228 OPENSSL_RAW_DATA,
229 $iv
230 );
231 $hmac = hash_hmac(
232 'sha256',
233 $ciphertext_raw,
234 $key,
235 true
236 );
237 return base64_encode( $iv . $hmac . $ciphertext_raw );
238 }
239
240 private function decrypt( $string ) {
241 $key = $this->get_encryption_key();
242 if ( null === $key ) {
243 return $string;
244 }
245 $c = base64_decode( $string );
246 $ivlen = openssl_cipher_iv_length( self::CIPHER );
247 $iv = substr( $c, 0, $ivlen );
248 $hmac = substr( $c, $ivlen, 32 );
249 $ciphertext_raw = substr( $c, $ivlen + 32 );
250 $calculated_hmac = hash_hmac(
251 'sha256',
252 $ciphertext_raw,
253 $key,
254 true
255 );
256 if ( !hash_equals( $hmac, $calculated_hmac ) ) {
257 return false;
258 }
259 return openssl_decrypt(
260 $ciphertext_raw,
261 self::CIPHER,
262 $key,
263 OPENSSL_RAW_DATA,
264 $iv
265 );
266 }
267
268 private function get_encryption_key() {
269 if ( defined( 'WPDA_ENCRYPT_AI_KEY' ) && '' !== trim( constant( 'WPDA_ENCRYPT_AI_KEY' ) ) ) {
270 return constant( 'WPDA_ENCRYPT_AI_KEY' );
271 } else {
272 return null;
273 }
274 }
275
276 }
277