Skip to content

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:

  • TRUE means the item positively matches.
  • FALSE means the hard filter rejects the item.
  • NULL means 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 (via AiContextScopeBase)
  • Store values in the scope map field through the base getValuesFromEntity() / form extractFormValues() 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_scope for 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:

  1. Its scoring influence is the plugin weight. Weights below 1 are treated as 1 during scoring only; display order still uses the declared weight.
  2. An item with no values receives full credit as generic context.
  3. Otherwise, its credit is the number of subscribed values also present on the item, divided by the number of subscribed values.
  4. 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
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() returns TRUE
  • isDynamic() returns FALSE
  • supportsSubscriptions() returns TRUE
  • Subclasses must implement doGetCurrentValue() (return NULL when the scope has no contextual auto-detection)
  • matchesCurrentContext() uses getCurrentValue() + getValuesFromEntity()
  • buildValueForm() renders checkboxes (multiple) or radios (single) from getAlteredValues()
  • getSelectedValueLabels() resolves labels through getAlteredValues()
  • getValuesFromEntity() reads effective scope: it resolves through AiContextItem::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.