Scope API
The scope system is built on Drupal's plugin API using PHP attributes for discovery.
Plugin attribute
use Drupal\ai_context\Attribute\AiContextScope;
#[AiContextScope(
id: 'my_scope',
label: new TranslatableMarkup('My Scope'),
description: new TranslatableMarkup('General description of the scope.'),
weight: 25,
)]
Namespace: Drupal\ai_context\Attribute\AiContextScope
The weight controls both display order and subscription scoring influence. Higher-weight scopes appear earlier and have greater scoring influence. Custom plugins can use intermediate values to appear between built-in scopes. Built-in weights are Global 100, Use Case 70, Entity Types 60, Site Section 50, Tag 40, Language 30, Taxonomy Terms 20, and Specific Entities 10. Global and Specific Entities do not participate in subscription scoring. The default weight is 1. Subscribable custom scopes should use positive weights (≥ 1). Weights below 1 are treated as 1 during subscription scoring only; display order still uses the declared weight.
Interface: AiContextScopeInterface
Namespace: Drupal\ai_context\Plugin\AiContextScope\AiContextScopeInterface
All scope plugins must implement this interface. Key methods:
Identity and metadata
| Method | Return | Description |
|---|---|---|
getId() |
string |
Plugin ID |
getLabel() |
string |
Human-readable label |
getDescription() |
string |
General description, shown on the scope overview and settings pages |
getItemFormDescription() |
string\|TranslatableMarkup |
Description shown on context item forms; defaults to getDescription() |
getSubscriptionDescription() |
string |
Description shown on agent subscription forms |
getWeight() |
int |
Display and subscription scoring weight |
Choosing a description method
A scope plugin can show different text in each place its description appears. Implement only the ones that need to differ; the rest fall back.
| Where it appears | Method used |
|---|---|
Scope overview page (/admin/config/ai/context/settings/scope) |
getDescription() |
| A scope's own settings page | getDescription() |
| Context item add/edit form | getItemFormDescription() |
| Agent subscription form | getSubscriptionDescription() |
getDescription() comes from the description parameter on the
#[AiContextScope] attribute, falling back to a string generated from the
label. getItemFormDescription() defaults to getDescription(), so a plugin
that does not override it behaves exactly as before.
Override getItemFormDescription() when guidance is specific to saving an
item and would be confusing elsewhere. The Global scope does this: its item
form warns that saving with Global enabled clears the item's other scope
values, which is meaningless on the overview page.
The overview and settings pages escape their text with Html::escape(), so
getDescription() must be plain text. getItemFormDescription() is rendered
as a form element #description, which Twig autoescapes like any other
printed value: a plain string gets Html::escape()'d, but a
\Drupal\Component\Render\MarkupInterface object (e.g. TranslatableMarkup)
is printed as-is, so returning one lets the description carry simple markup
such as <br>.
Value management
| Method | Return | Description |
|---|---|---|
getValues() |
array |
Raw value_id => label pairs from the plugin |
getAlteredValues() |
array |
getValues() after hook_ai_context_scope_values_alter(); use this when building forms, filters, or resolving labels |
allowsMultiple() |
bool |
Whether multiple values can be selected |
isDynamic() |
bool |
Whether values come from external source |
getSelectedValueLabels(array) |
array |
Labels for selected value IDs |
getValuesFromEntity(AiContextItem) |
array |
Effective scope values for an item — resolves parent inheritance (see Effective scope and parent inheritance) |
Contextual detection
| Method | Return | Description |
|---|---|---|
getCurrentValue() |
?string |
Current value from request context |
matchesCurrentContext(AiContextItem) |
?bool |
Whether item matches current context |
matchesCurrentContext() is a tri-state contract:
TRUEmeans the item positively matches.FALSEmeans the hard filter rejects the item.NULLmeans the scope is neutral because the item is unrestricted.
Entity Types and Site Section fail closed: if the item has values but no
current entity or request path can be resolved, they return FALSE rather
than NULL. Other scopes may still return NULL when the current value
cannot be determined.
The resolver removes an item when any enabled scope returns FALSE.
Agent subscriptions
| Method | Return | Description |
|---|---|---|
supportsSubscriptions() |
bool |
Whether this scope participates in agent subscription UI |
When supportsSubscriptions() returns FALSE, the scope is hidden on agent
subscription forms (via ai_context.scope_subscription_form and the agent
settings form). The same flag is also used internally by
AiContextScopeResolver: non-subscription scopes whose
matchesCurrentContext() returns TRUE may be auto-included in context
selection (for example, entity item match on the current page). Global scope
returns FALSE and is handled separately.
Custom scope authors should treat these as two related behaviors today; a future release may split them into explicit capability methods.
Extension boundary for 1.0
Supported for custom scope plugins in 1.0:
- Implement
AiContextScopeInterface(viaAiContextScopeBase) - Store values in the scope map field through the base
getValuesFromEntity()/ formextractFormValues()flow - Use documented hooks and alters
Not a supported extension surface in 1.0:
- Custom entity-field storage (the built-in entity item scope is internal)
- Direct participation in the scope index pipeline, entity-delete cleanup, or presave validation (map-backed scopes are indexed automatically)
- Injecting
plugin.manager.ai_context_scopefor orchestration (internal)
Built-in scope plugins expose a PLUGIN_ID constant matching their plugin ID.
Use those constants in module code when referring to core scopes; do not
hardcode scope ID strings in new code.
Scope scoring
AiContextScopeResolver::calculateScopeScore() compares an item's effective
scope values with each non-empty subscription. Disabled scopes, empty
subscription arrays, and non-subscription scopes
(supportsSubscriptions() is FALSE, including Global and Specific
Entities) are skipped.
For each participating scope:
- Its scoring influence is the plugin weight. Weights below 1 are treated as 1 during scoring only; display order still uses the declared weight.
- An item with no values receives full credit as generic context.
- Otherwise, its credit is the number of subscribed values also present on the item, divided by the number of subscribed values.
- The final score is the weighted credit divided by the maximum available weighted credit.
An item with a final score of zero is excluded from the scored group. A zero match for one scope does not necessarily exclude it because another scope can contribute positive credit.
The built-in scoring influences are currently:
- Use Case: 70
- Entity Types: 60
- Site Section: 50
- Tag: 40
- Language: 30
- Taxonomy Terms: 20
These numbers are internal implementation details rather than a stable public API. Changing a plugin's definition weight can affect both plugin ordering and subscription scoring.
See Context Selection for the site-builder-facing explanation and examples.
Form building
| Method | Description |
|---|---|
buildValueForm(...) |
Builds checkboxes/radios for scope values |
extractFormValues(mixed) |
Normalizes submitted form values |
buildSettingsForm(...) |
Builds per-scope settings form |
validateSettingsForm(...) |
Validates settings submission |
submitSettingsForm(...) |
Processes settings submission |
Configuration
| Method | Return | Description |
|---|---|---|
getConfigName() |
string |
Config object name for this scope |
defaultConfiguration() |
array |
Default settings (must include enabled) |
isEnabled() |
bool |
Whether scope is currently enabled |
Management links
| Method | Return | Description |
|---|---|---|
getManageRoute() |
?array |
Route info array (route_name, route_parameters) to manage scope values |
getManageLabel() |
?string |
Link text for the management link |
Base class: AiContextScopeBase
Namespace: Drupal\ai_context\Plugin\AiContextScope\AiContextScopeBase
Abstract base class providing default implementations for all interface
methods except getValues(). Extend this class for custom scope plugins.
Key defaults:
allowsMultiple()returnsTRUEisDynamic()returnsFALSEsupportsSubscriptions()returnsTRUE- Subclasses must implement
doGetCurrentValue()(returnNULLwhen the scope has no contextual auto-detection) matchesCurrentContext()usesgetCurrentValue()+getValuesFromEntity()buildValueForm()renders checkboxes (multiple) or radios (single) fromgetAlteredValues()getSelectedValueLabels()resolves labels throughgetAlteredValues()getValuesFromEntity()reads effective scope: it resolves throughAiContextItem::getScopeItem(), so a subcontext item that inherits its parent's scope returns the parent's values (see below)
Effective scope and parent inheritance
A subcontext item can inherit its parent's scope instead of storing its
own. When it does, its own scope map is empty and the effective values live on
the parent. Resolution goes through AiContextItem::getScopeItem(), which
returns the parent for an inheriting child and $this otherwise.
The base getValuesFromEntity() already resolves this for you:
public function getValuesFromEntity(AiContextItem $item): array {
// Reads the parent's values when $item inherits parent scope.
return $item->getScopeItem()->getScopeValues($this->getId());
}
If you override getValuesFromEntity() in a custom scope plugin, resolve
through getScopeItem() — do not read $item->getScopeValues() (or
$item->getScope()) directly. Reading the item's own map bypasses parent
inheritance: for an inheriting child the map is empty, so your scope would
silently contribute nothing while every other scope still reflects the
parent's values, producing an inconsistent effective scope.
// Correct — honors parent inheritance.
public function getValuesFromEntity(AiContextItem $item): array {
$values = $item->getScopeItem()->getScopeValues($this->getId());
// ...any custom post-processing...
return $values;
}
// Wrong — an inheriting child returns [] here even though its parent is scoped.
public function getValuesFromEntity(AiContextItem $item): array {
return $item->getScopeValues($this->getId());
}
The distinction between effective and stored scope is intentional and mirrored across the entity API. Effective accessors resolve inheritance; stored accessors read the item's own field values (used by edit forms, scope summaries, and persistence limits):
| Effective (resolves inheritance) | Stored (this item only) |
|---|---|
getScopeItem()->getScopeValues() |
getScopeValues() / getScope() |
isGlobal() |
isStoredGlobal() |
getEntityItems() / hasEntityItems() |
getStoredEntityItemScopeValues() / hasStoredEntityItems() |
matchesEntityItem() |
— |
The scope index (AiContextScopeIndexService::indexItem()) also indexes
effective scope, so SQL prefiltering matches runtime selection.
Plugin manager: AiContextScopeManager
Namespace: Drupal\ai_context\AiContextScopeManager
Service ID: plugin.manager.ai_context_scope
Status: Internal — see API stability. Contrib and site code should not depend on manager orchestration methods. The subscription form builder service is the supported entry point for subscription UI.
Discovers and manages scope plugins. getScopePlugins() returns all plugins
in descending weight order, with plugin ID used to break ties. Core and custom
plugins are interleaved according to their declared weight.
getScopePluginDisplayWeights() exposes the same order as sequential
#weight values for forms and tabs. getScopeIconClass() is
module-internal UI plumbing (definition metadata, built-in map, then default
fallback); contrib code should declare icon_class on custom scopes instead.
Plugin weight controls both UI placement and the scoring influence
described above. Scope plugin definitions can be altered via
hook_ai_context_scope_info_alter(); see Hooks.
Scope subscriptions on custom forms
When a module needs scope subscription widgets outside the agent settings
form, inject or load the public
ai_context.scope_subscription_form service
(AiContextScopeSubscriptionFormBuilderInterface). It wraps the internal
scope plugin manager and provides buildWidgets(), extractValues(), and
buildSummary() with the same behavior as the agent form. See
Services — ai_context.scope_subscription_form.