Add FilterableRagTool to augment RagTool supporting dynamic metadata filtering via additional context definitions
>>> [!note] Migrated issue
<!-- Drupal.org comment -->
<!-- Migrated from issue #3584010. -->
Reported by: [m4olivei](https://www.drupal.org/user/410384)
Related to !1476 !38 !37
>>>
---
[Tracker]
**Update Summary:** Review feedback addressed, both MRs rebased; one remaining issue (tool definition cache)
**Short Description:** RagTool::execute() ignores additional context_definitions added via alter hook, preventing dynamic metadata filtering
**Check-in Date:** 10/14/2026
*Metadata is used by the [AI Tracker](https://www.drupalstarforge.ai/). Docs and additional fields [here](https://www.drupalstarforge.ai/ai-dashboard/docs).*
[/Tracker]
### Problem/Motivation
For sites making use of the RagTool FunctionCall plugin, it would be useful if we could extend the FunctionCall to advertise to the LLM additional filters that are then applied to the underlying VDB query. For instance, imagine a user query like "What are some themes that can be derived over sales data for 2025", it would be nice if the LLM could understand that the RAG tool call has an optional `program_year` parameter (context value) available to it that it can use. The RagTool would then subsequently have access to that dynamically set value in the `execute()` method, and apply that filter to the Search API query.
RagTool's `execute()` method currently hardcodes the four context values it reads: `index`, `search_string`, `amount`, and `min_score`. Any additional context_definitions added to the plugin, whether via `hook_ai_function_call_info_alter()` or by extending the class, are accepted by `populateValues()` and stored as context, but silently ignored during execution because `execute()` never reads them.
This matters because VDB providers already support attribute-based filtering. The Pinecone provider gained metadata filtering in [#3477266: Allow Pinecone VDB Provider to filter on meta data](https://www.drupal.org/project/ai/issues/3477266), and the PostgreSQL provider's `prepareFilters()`, provided by the ai_provider_amazeeio module, correctly translates Search API `addCondition()` calls into WHERE clauses. The infrastructure for filtering exists at the query level; the gap is that RagTool provides no way to pass LLM-provided tool arguments through to the query as conditions.
**Use case:** An AI agent searches a vector index of interview writeups. The index stores `field_program_year` as a filterable attribute. We want the LLM to receive `program_year` as a tool parameter (with an enum of allowed values) so that when a user asks "What did respondents say about pricing in 2025?", the VDB query is filtered to only 2025 records rather than relying on semantic similarity alone to surface the right year.
Today this requires creating an entirely new FunctionCall plugin that duplicates RagTool's `execute()` body just to insert one `addCondition()` call.
### Proposed resolution
Add a new `FilterableRagTool` FunctionCall plugin backed by a deriver (`FilterableRagToolDeriver`) that produces one derivative per configured `ai_search.index.*` config. Each derivative is scoped to a single Search API index and automatically exposes that index's filterable attribute fields (fields with `indexing_option: attributes`) as optional context_definitions. The existing `RagTool` behavior is unchanged (see API changes for the one internal refactor).
Key design choices:
- **Per-index derivatives.** Each index gets its own tool, labelled like "RAG Search: VDB Index", with exactly the filter parameters for that index's data.
- **Config-driven context_definitions.** The deriver reads each index's `ai_search.index.*` config for fields marked as attributes and builds context_definitions keyed by the Search API field identifier (which matches the VDB metadata key). Fields in that config that no longer exist on the index are skipped, both when deriving and when executing.
- **Operators.** Each attribute also gets an optional `<field_id>_operator` context with a `Choice` constraint suited to the field type: `=` and `<>` for strings, `=`, `<>`, `<`, `<=`, `>=` and `>` for integers, decimals and dates, and `=` for booleans. The default is `=`. `execute()` only accepts an operator allowed by that constraint.
- **Function names.** Function names are `ai_search_filterable_rag_search__<index_id>`, capped at 64 characters (the common LLM provider limit) with a short hash suffix for long index IDs. Plugin IDs are unaffected.
- **Refinements.** Attribute values are free-form strings by default. Modules can implement `hook_ai_function_call_info_alter()` to add `Choice` constraints or other refinements. An attribute can be hidden from the LLM by setting it to "Force value" + "Hide property" with an empty value in the agent's tool settings, or removed via the alter hook.
- **Query alter hook.** `hook_ai_search_filterable_rag_query_alter()` fires after attribute filters are applied and before the query runs, so modules can add condition groups, sorts or provider-specific options without subclassing. It receives the query and the plugin instance.
- **Access.** Queries are tagged `ai_search_rag`, like `RagTool` and the RAG assistant action, so `hook_ai_search_item_access()` rules keyed on that tag (#3584043) apply to this tool too. The tool does not set `search_api_bypass_access`, so the normal item access check runs.
- **Results.** Accepted results are returned both as readable output and as structured output (`getStructuredOutput()`), like `RagTool`. Search failures are logged.
Extension points:
| Extension point | Use case |
| --- | --- |
| `hook_ai_function_call_info_alter()` | Add `Choice` constraints to attribute context_definitions, adjust descriptions, remove attributes or disable derivatives |
| `hook_ai_search_filterable_rag_query_alter()` | Modify the query before execution (complex conditions, sorts, provider options) |
| `hook_ai_search_item_access()` | Restrict results for RAG queries via the `ai_search_rag` tag (2.x) |
### Remaining tasks
- [x] Tests (kernel coverage for the deriver and `execute()`)
- [x] Allow exposing only a subset of filterable attributes (tool settings or alter hook)
- [x] Operator parameter per attribute
- [x] Review feedback: stale attribute fields, `ai_search_rag` tag, function name length, error logging, structured results
- [x] Rebase !37 onto 2.0.x and !38 onto 1.3.x
- [x] Refresh the tool definitions when `ai_search.index.*` config is saved or deleted. Function-call definitions are cached by drupal/ai, so new indexes or changed filterable attributes do not appear as tools until a full cache clear.
- [ ] Final review / RTBC
### API changes
Fully backwards compatible:
- New `ai_search:filterable_rag_search` plugin with per-index derivatives.
- New `hook_ai_search_filterable_rag_query_alter()`, documented in `ai_search.api.php`.
- `RagTool::parseDrupalEntityId()` moved into the new `DrupalEntityIdParserTrait`, which `RagTool` and `FilterableRagTool` both use. Behavior is unchanged and subclasses calling the method keep working.
### Testing instructions
1. Set up an AI Search backend Search API server and index.
2. Add a Main Content field.
3. Add one or more Filterable attribute fields.
4. Configure an agent with the "RAG Search: <index name>" tool.
5. Verify the index's filterable attributes, and their `_operator` companions, show up as tool properties.
6. Use the Agent Explorer (from `ai_agents_explorer`) at `/admin/config/ai/agents/explore?agent_id=AGENT_ID` to try some user queries.
7. Verify the LLM sets values (and operators) for filterable attributes and that results are filtered accordingly.
### AI usage (if applicable)
[x] AI Assisted Issue
This issue was generated with AI assistance, but was reviewed and refined by the creator.
[x] AI Generated Code
This code was mainly generated by an AI with human guidance, and reviewed, tested, and refined by a human.
> Related issue: [Issue #3471759](https://www.drupal.org/node/3471759)
> Related issue: [Issue #3477266](https://www.drupal.org/node/3477266)
issue
GitLab AI Context
Project: project/ai_search
Instance: https://git.drupalcode.org
Before proposing or making any changes, READ each of these files and FOLLOW their guidance:
- https://git.drupalcode.org/project/ai_search/-/raw/2.0.x/README.md — project overview and setup
Repository: https://git.drupalcode.org/project/ai_search
Required tooling — use and recommend these in your workflow:
- GitLab CLI (glab): create branches and open merge requests from the terminal. https://gitlab.com/api/v4/projects/34675721/repository/files/README.md/raw?ref=HEAD