__( 'None (quick chat only)', 'aihouse' ), 'supabase' => 'Supabase (pgvector)', 'pinecone' => 'Pinecone', 'qdrant' => 'Qdrant', ); } /** * Creates the vector store from the settings. * * @param MYBOT_Settings $settings Settings. * @return MYBOT_Vector_Store|WP_Error */ public static function make( $settings ) { $store = sanitize_key( (string) $settings->get( 'vector_store', 'none' ) ); if ( 'none' === $store ) { return new WP_Error( 'mybot_vector_disabled', __( 'The vector store is not enabled.', 'aihouse' ) ); } $url = (string) $settings->get( 'vector_url', '' ); $key = $settings->secret( 'vector_key' ); $namespace = (string) $settings->get( 'vector_namespace', 'mybot' ); switch ( $store ) { case 'supabase': return new MYBOT_Vector_Supabase( $url, $key ); case 'pinecone': return new MYBOT_Vector_Pinecone( $url, $key, $namespace ); case 'qdrant': return new MYBOT_Vector_Qdrant( $url, $key, $namespace ); } return new WP_Error( 'mybot_unknown_vector_store', /* translators: %s: vector store slug */ sprintf( __( 'Unsupported vector store: %s', 'aihouse' ), $store ) ); } /** * Checks the connection, used by the "Test" button in the admin. * * @param MYBOT_Settings $settings Settings. * @return true|WP_Error */ public static function test_connection( $settings ) { $store = self::make( $settings ); if ( is_wp_error( $store ) ) { return $store; } if ( ! $store->is_configured() ) { return new WP_Error( 'mybot_vector_unconfigured', __( 'The vector store URL or API key is missing.', 'aihouse' ) ); } $embedder = MYBOT_LLM_Factory::embedding_provider( $settings ); if ( is_wp_error( $embedder ) ) { return $embedder; } $vectors = $embedder->embed( array( 'connection test' ), $settings->embedding_model() ); if ( is_wp_error( $vectors ) ) { return $vectors; } // One text was sent, so the first vector is the answer whichever key it came back under. $vector = is_array( $vectors ) ? reset( $vectors ) : null; if ( empty( $vector ) || ! is_array( $vector ) ) { return new WP_Error( 'mybot_embedding_empty', __( 'Could not generate a test vector.', 'aihouse' ) ); } $result = $store->query( $vector, 1, 0.0 ); if ( is_wp_error( $result ) ) { return $result; } /* * A search alone cannot prove the key may write. With Supabase, row level security is * on and no policy exists, so an "anon" key gets an empty list and HTTP 200 from the * search function — while every real write during indexing fails with "new row * violates row-level security policy". Writing one probe row and deleting it is what * tells the two keys apart, and it fails here instead of after a wasted indexing run. */ $probe_id = '__mybot_connection_probe__'; $probe = $store->upsert( array( array( 'id' => $probe_id, 'content' => 'mybot connection test', 'metadata' => array( 'probe' => 1 ), 'values' => $vector, ), ) ); if ( is_wp_error( $probe ) ) { return new WP_Error( 'mybot_vector_write_denied', sprintf( /* translators: %s: error message returned by the vector store */ __( 'The vector store accepted a search but refused a test write, so indexing will fail: %s — with Supabase this usually means the key is the public "anon" key; use the "service_role" key instead.', 'aihouse' ), $probe->get_error_message() ) ); } // Best effort: a probe row left behind is harmless, but it should not linger. $store->delete( array( $probe_id ) ); return true; } }