settings = $settings; parent::__construct( 'vector-search', 'Retrieves relevant documentation from the knowledge base to answer conceptual questions — what a feature is, how it works, pricing, compatibility, or background context. Use this tool ONLY when the user wants to understand something, NOT when they want to do something or navigate somewhere. Do NOT call this tool for action-oriented questions (how to create, change, configure, manage, enable, disable, add, edit, or delete something in the admin panel) — those must be answered using search-admin-pages instead. Extract key concepts from the user\'s question as the search query.' ); } /** * Whether the credentials this tool needs are configured. * * Queries Pinecone directly and embeds the query locally, so it needs a * Pinecone key + index URL and the API key for the configured embedding * provider. When any is missing the agent should not offer the tool at all * rather than let the model call a tool that can only fail. * * @param object $settings Settings instance. * @return bool */ public static function is_configured( $settings ): bool { $pinecone_key = (string) $settings->get( 'vectorDBProviders.pinecone.key', '' ); $index_url = (string) $settings->get( 'vectorDBProviders.pinecone.indexUrl', '' ); if ( empty( $pinecone_key ) || empty( $index_url ) ) { return false; } // The query embedding is generated locally, so the matching provider key must exist. switch ( $settings->get( 'vectorDBProviders.pinecone.embeddingProvider', 'gemini' ) ) { case 'gemini': $embed_key = (string) $settings->get( 'llmProviders.google.apiKey', '' ); break; case 'openai': $embed_key = (string) $settings->get( 'llmProviders.openai.apiKey', '' ); break; default: return false; } return ! empty( $embed_key ); } /** * Define tool properties * * @return array */ protected function properties(): array { return [ new ToolProperty( name: 'query', type: PropertyType::STRING, description: 'Search keywords extracted from the user\'s question. Use the main concepts and key terms. Example: "How do I create an event in Eventin?" → query="create event eventin". "What are Eventin ticket types?" → query="eventin ticket types".', required: true ), new ToolProperty( name: 'k', type: PropertyType::NUMBER, description: 'Number of documentation chunks to retrieve (1-10, default 5). Use 3-5 for simple questions, up to 10 for complex ones.', required: false ), new ToolProperty( name: 'plugin', type: PropertyType::STRING, description: 'Optional documentation scope. Pass the plugin or theme key stated in the system prompt: a plugin slug (eventin, wpcafe, timetics, booktics, pointics, aisentic) or a Themewinter theme key of the form "-theme" (e.g. "cafesio-theme"). Restricts results to that source. Omit to search all documentation.', required: false ), ]; } /** * Execute the tool * * @param string $query Search query. * @param int|null $k Number of results. * @param string|null $plugin Plugin/theme key to scope the search to. * @return array */ public function __invoke(?string $query = null, ?int $k = null, ?string $plugin = null): array { try { if (empty($query)) { return [ 'matches' => [], 'total' => 0, 'warning' => 'No search query provided', ]; } $k = max( 1, min( 10, $k ?? 5 ) ); // Generate embedding for the query using configured provider $factory = new Embeddings_Factory($this->settings); $embeddings = $factory->create(); $vector = $embeddings->embedText($query); // Optionally scope to one plugin/theme via the 'plugin' metadata field. $filter = $this->build_plugin_filter($plugin); $pinecone = new Pinecone_Client($this->settings); $results = $pinecone->query($vector, $k, $filter); // Format results (strip 'link' to prevent AI from including external URLs in responses) $matches = array_map(function ($result) { $metadata = $result['metadata'] ?? []; unset( $metadata['link'] ); // Defensively cap chunk text in case a remote vector exceeds the ingest limit. if ( ! empty( $metadata['content'] ) && is_string( $metadata['content'] ) ) { $metadata['content'] = wp_html_excerpt( $metadata['content'], 500, '…' ); } return [ 'id' => $result['id'], 'score' => $result['score'], 'metadata' => $metadata, ]; }, $results); return [ 'matches' => $matches, 'total' => count($matches), ]; } catch (\Exception $e) { Log::error( 'VectorSearchTool failed', [ 'error' => $e->getMessage() ] ); return [ 'error' => $e->getMessage() ]; } } /** * Build a Pinecone metadata filter that scopes results to one plugin or theme. * * @param string|null $plugin Plugin slug or theme key ("-theme"). * @return array Pinecone filter, or [] when no valid scope was supplied. */ private function build_plugin_filter(?string $plugin): array { if (empty($plugin)) { return []; } $normalized = strtolower(trim($plugin)); if (!in_array($normalized, $this->allowed_scopes(), true)) { return []; } return [ 'plugin' => [ '$eq' => $normalized ] ]; } /** * All accepted scope values: base plugins plus theme documentation keys. * * @return string[] */ private function allowed_scopes(): array { return array_merge( Screen_Id_To_Plugin_Mapper::get_integration_plugins(), Active_Theme_Resolver::get_all_theme_keys() ); } }