Add a custom Storefront Assistant agent
Edit on GitHubStorefront Assistant ships with the Product Discovery agent. You can add your own agents to handle specific kinds of questions with a dedicated system prompt and tool list. Each agent has its own toggle in the Back Office, and customers can select it in the chat.
This document shows how to add a Gift Advisor agent that helps customers find a gift with a subset of the existing catalog tools.
Prerequisites
How agent selection works
AgentSelector selects one agent for each chat turn:
- It skips each agent whose toggle, returned by
getEnabledConfigurationKey(), is off. - If the customer selected an agent in the chat, the enabled agent with this name handles the turn.
- If the customer selected Auto, the first enabled agent in the plugin stack whose
isApplicable()method returnstruehandles the turn. - If no agent is found, the turn ends with the
ai_commerce.storefront_assistant.error.no_agent_availableerror.
Register a specialized agent before ProductDiscoveryAgentPlugin, and make its isApplicable() method strict. ProductDiscoveryAgentPlugin::isApplicable() always returns true, so in Auto mode, an agent after it is never selected.
The agent contract
An agent implements SprykerFeature\Client\AiCommerce\Dependency\StorefrontAssistant\StorefrontAssistantAgentPluginInterface:
| METHOD | DESCRIPTION |
|---|---|
getName(): string |
Returns a unique name, for example gift_advisor. Use lowercase letters and underscores. The name is also part of the glossary keys of the agent. |
getDescription(): string |
Returns a short description. The chat uses it when the agent.<name>.description glossary key is missing. |
isApplicable(StorefrontAssistantChatRequestTransfer): bool |
Returns true when the agent must handle the turn in Auto mode. |
getEnabledConfigurationKey(): string |
Returns the Configuration Management key of the agent toggle. |
executeAgent(StorefrontAssistantChatRequestTransfer): PromptResponseTransfer |
Builds the prompt request, calls AiFoundationClientInterface::streamPrompt(), and returns the response. For details on streamPrompt(), see Stream AI responses with the AiFoundation module. |
StorefrontAssistantChatRequestTransfer gives the agent the following data:
| PROPERTY | CONTENT |
|---|---|
message |
The text of the customer. |
conversationReference |
The conversation reference that the browser sent. It is not scoped to the customer. |
customerReference |
The reference of the logged-in customer. |
localeName |
The locale of the current request. |
attachments |
The validated attachments. |
selectedAgent |
The agent that the customer selected, or an empty value for Auto. |
storefrontAssistantPageContext |
The product, category, or search context of the current page. |
The PromptRequestTransfer that the agent sends must contain the following fields:
| FIELD | VALUE |
|---|---|
aiConfigurationName |
The AI configuration name. Zed resolves the vendor and the model from it. |
conversationReference |
The conversation reference scoped to the customer, from ConversationReferenceDeriverInterface::deriveConversationReference(). |
toolNames |
The tool names of the agent. Each name must belong to a plugin in the Client AiFoundationDependencyProvider::getAiToolPlugins(). |
systemPrompt |
The configured system prompt with the page context. |
promptMessage |
The customer message with the attachments. |
StorefrontAssistantRequestMapperInterface::mapStorefrontAssistantChatRequestToPromptRequest() sets all of these fields. Use this mapper instead of building the transfer manually.
Always derive the conversation reference with ConversationReferenceDeriverInterface. Do not send the raw browser reference. With a raw reference, the history endpoints cannot read the messages, and the history is not scoped to the customer.
1) Add the constants
Define the Configuration Management keys of the agent toggle and the system prompt:
src/Pyz/Shared/AiCommerce/AiCommerceConstants.php
/**
* Configuration Management key of the Gift Advisor agent toggle.
*
* @api
*/
public const string CONFIGURATION_KEY_STOREFRONT_ASSISTANT_GENERAL_IS_GIFT_ADVISOR_AGENT_ENABLED = 'ai_commerce:storefront_assistant:general:is_gift_advisor_agent_enabled';
/**
* Configuration Management key of the Gift Advisor agent system prompt.
*
* @api
*/
public const string CONFIGURATION_KEY_STOREFRONT_ASSISTANT_GIFT_ADVISOR_SYSTEM_PROMPT = 'ai_commerce:storefront_assistant:system_prompts:gift_advisor_system_prompt';
2) Add the Back Office settings
Add the toggle and the system prompt of the agent to the storefront_assistant tab:
data/configuration/ai_commerce.configuration.yml
- key: general
# ...
settings:
# ... is_enabled, is_product_discovery_agent_enabled ...
- key: is_gift_advisor_agent_enabled
name: Enable Gift Advisor Agent
description: When enabled, the Storefront Assistant can use the Gift Advisor agent.
type: boolean
default_value: 'true'
enabled: true
secret: false
storefront: true
order: 2
scopes:
- global
- key: system_prompts
# ...
settings:
# ... product_discovery_system_prompt ...
- key: gift_advisor_system_prompt
name: Gift Advisor System Prompt
description: System prompt of the Gift Advisor agent.
type: text
default_value: "You are the gift advisor of an online shop. Ask for the recipient, the occasion and the budget if the customer did not give them. Ground every product fact in tool results from the current turn. Show products only with display_products."
enabled: true
secret: false
storefront: true
order: 1
scopes:
- global
Consider the following when you add the settings:
- Set
storefront: trueon both settings. The agent runs in the Client layer and reads only Storefront settings. - Always give the system prompt a
default_value. When the value is blank,AiCommerceConfig::getStorefrontAssistantSystemPrompt()returns the default prompt of the Product Discovery agent. - Sync the configuration before you test. A toggle key that is not synced resolves to disabled, and the agent is not displayed in the chat.
3) Add the Client configuration methods
Add the getters for the configuration keys and the tool list of the agent:
src/Pyz/Client/AiCommerce/AiCommerceConfig.php
public function getGiftAdvisorAgentEnabledKey(): string
{
return AiCommerceConstants::CONFIGURATION_KEY_STOREFRONT_ASSISTANT_GENERAL_IS_GIFT_ADVISOR_AGENT_ENABLED;
}
public function getGiftAdvisorSystemPromptKey(): string
{
return AiCommerceConstants::CONFIGURATION_KEY_STOREFRONT_ASSISTANT_GIFT_ADVISOR_SYSTEM_PROMPT;
}
/**
* @return list<string>
*/
public function getGiftAdvisorAgentToolNames(): array
{
return [
CatalogSearchToolPlugin::TOOL_NAME,
CategoryTreeToolPlugin::TOOL_NAME,
ProductDetailsToolPlugin::TOOL_NAME,
DisplayProductsToolPlugin::TOOL_NAME,
OfferChoicesToolPlugin::TOOL_NAME,
];
}
Each tool in this list must be registered in the Client AiFoundationDependencyProvider::getAiToolPlugins(). A customer-facing tool must also be in getStorefrontAssistantCustomerFacingToolNames().
4) Add the agent plugin
src/Pyz/Client/AiCommerce/Plugin/StorefrontAssistant/GiftAdvisorAgentPlugin.php
<?php
declare(strict_types = 1);
namespace Pyz\Client\AiCommerce\Plugin\StorefrontAssistant;
use Generated\Shared\Transfer\PromptResponseTransfer;
use Generated\Shared\Transfer\StorefrontAssistantChatRequestTransfer;
use Spryker\Client\Kernel\AbstractPlugin;
use Spryker\Client\Kernel\BundleConfigResolverAwareTrait;
use SprykerFeature\Client\AiCommerce\Dependency\StorefrontAssistant\StorefrontAssistantAgentPluginInterface;
/**
* @method \SprykerFeature\Client\AiCommerce\AiCommerceFactory getFactory()
* @method \Pyz\Client\AiCommerce\AiCommerceConfig getConfig()
*/
class GiftAdvisorAgentPlugin extends AbstractPlugin implements StorefrontAssistantAgentPluginInterface
{
use BundleConfigResolverAwareTrait;
protected const string AGENT_NAME = 'gift_advisor';
protected const string AGENT_DESCRIPTION = 'Helps the customer to find a gift for a recipient, an occasion and a budget.';
protected const array GIFT_KEYWORDS = ['gift', 'present', 'geschenk'];
/**
* {@inheritDoc}
*
* @api
*/
public function getName(): string
{
return static::AGENT_NAME;
}
/**
* {@inheritDoc}
*
* @api
*/
public function getDescription(): string
{
return static::AGENT_DESCRIPTION;
}
/**
* {@inheritDoc}
* - Applies when the customer message names a gift.
*
* @api
*/
public function isApplicable(StorefrontAssistantChatRequestTransfer $storefrontAssistantChatRequestTransfer): bool
{
$message = mb_strtolower((string)$storefrontAssistantChatRequestTransfer->getMessage());
foreach (static::GIFT_KEYWORDS as $giftKeyword) {
if (str_contains($message, $giftKeyword)) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*
* @api
*/
public function getEnabledConfigurationKey(): string
{
return $this->getConfig()->getGiftAdvisorAgentEnabledKey();
}
/**
* {@inheritDoc}
*
* @api
*/
public function executeAgent(
StorefrontAssistantChatRequestTransfer $storefrontAssistantChatRequestTransfer
): PromptResponseTransfer {
$factory = $this->getFactory();
$config = $this->getConfig();
$scopedConversationReference = $factory->createConversationReferenceDeriver()->deriveConversationReference(
(string)$storefrontAssistantChatRequestTransfer->getCustomerReference(),
(string)$storefrontAssistantChatRequestTransfer->getConversationReference(),
);
$systemPrompt = $factory->createPageContextSystemPromptComposer()->composeSystemPrompt(
$config->getStorefrontAssistantSystemPrompt($config->getGiftAdvisorSystemPromptKey()),
$storefrontAssistantChatRequestTransfer,
);
$promptRequestTransfer = $factory->createStorefrontAssistantRequestMapper()
->mapStorefrontAssistantChatRequestToPromptRequest(
$storefrontAssistantChatRequestTransfer,
$config->getStorefrontAssistantAiConfigurationName(),
$scopedConversationReference,
$config->getGiftAdvisorAgentToolNames(),
$systemPrompt,
);
return $factory->getAiFoundationClient()->streamPrompt($promptRequestTransfer);
}
}
The Client AbstractPlugin has no getConfig() method. To read the configuration, add BundleConfigResolverAwareTrait, as ProductDiscoveryAgentPlugin does.
The executeAgent() method uses the create*() methods of the module factory, so the agent needs no project-level factory. It derives the scoped conversation reference, composes the system prompt with the page context, and maps the chat request to a prompt request.
In this example, isApplicable() checks keywords to keep the example short. A production agent can use the page context, the attachments, or a short classification prompt.
Follow these rules in executeAgent():
- Return the
PromptResponseTransferofstreamPrompt(). If the turn fails, return an unsuccessful response with errors. Storefront Assistant changes it into anerrorstream event. - Do not write to the output. The SSE plugins stream the text, the reasoning, and the tool events.
- Do not throw an exception for an expected failure. Return an unsuccessful response instead.
5) Register the agent
Add the plugin to the agent stack before ProductDiscoveryAgentPlugin:
src/Pyz/Client/AiCommerce/AiCommerceDependencyProvider.php
use Pyz\Client\AiCommerce\Plugin\StorefrontAssistant\GiftAdvisorAgentPlugin;
use SprykerFeature\Client\AiCommerce\Plugin\StorefrontAssistant\ProductDiscoveryAgentPlugin;
/**
* @return array<\SprykerFeature\Client\AiCommerce\Dependency\StorefrontAssistant\StorefrontAssistantAgentPluginInterface>
*/
protected function getStorefrontAssistantAgentPlugins(): array
{
return [
new GiftAdvisorAgentPlugin(),
new ProductDiscoveryAgentPlugin(),
];
}
6) Add translations
Add the label and the description of the agent for each store locale:
data/import/common/common/glossary.csv
>ai_commerce.storefront_assistant.agent.gift_advisor.label,Gift Advisor,en_US
ai_commerce.storefront_assistant.agent.gift_advisor.label,Geschenkberater,de_DE
ai_commerce.storefront_assistant.agent.gift_advisor.description,"Helps you find a gift for a person, an occasion and a budget.",en_US
ai_commerce.storefront_assistant.agent.gift_advisor.description,"Hilft Ihnen, ein Geschenk für eine Person, einen Anlass und ein Budget zu finden.",de_DE
If these keys are missing, the chat shows the agent name in a readable form, such as Gift Advisor, and the English text of getDescription().
7) Apply the changes
console cache:class-resolver:build
console configuration:sync
console data:import glossary
console queue:worker:start --stop-when-empty
console cache:empty-all
Make sure the following applies:
- When you open the chat as a logged-in customer, the agent selector shows Auto, Gift Advisor, and Product Discovery.
- When you select Auto and send “I need a gift for my father”, the agent badge shows Gift Advisor.
- When you select Auto and send “Show me cameras”, the agent badge shows Product Discovery.
- When you select Gift Advisor and send any message, the Gift Advisor agent answers.
- When you turn off Enable Gift Advisor Agent in the Back Office, the agent disappears from the selector, and Auto uses Product Discovery.
- After you reload the page and open the conversation, the messages of the Gift Advisor agent are displayed in the history.
Optional: Use a separate AI configuration for the agent
The example agent uses the same AI vendor and model as the Product Discovery agent. To give the agent its own model, do all of the following steps. If you skip a step, the agent fails or streams nothing.
- Add the AI configuration constants and the model setting of the agent, as described in Install Storefront Assistant.
- Add the AI configuration to
config/Shared/config_ai.php. - Add the AI configuration name to
getClientResolvableAiConfigurationNames()in the ZedAiFoundationConfig. Without it, Zed rejects the turn. - Add the AI configuration name to
getStorefrontAssistantSseAiConfigurationNames()in the ClientAiCommerceConfig. Without it, the SSE plugins ignore the turn: the model runs, but the customer sees no text and no tool events. - In
executeAgent(), pass the new AI configuration name tomapStorefrontAssistantChatRequestToPromptRequest().
src/Pyz/Client/AiCommerce/AiCommerceConfig.php
/**
* @return array<string>
*/
public function getStorefrontAssistantSseAiConfigurationNames(): array
{
return [
$this->getStorefrontAssistantAiConfigurationName(),
$this->getGiftAdvisorAiConfigurationName(),
];
}
Optional: Add a custom tool for the agent
A tool is a function that the model can call during a chat turn. The agent in this guide uses only the tools of the module. To give the Gift Advisor agent its own data or logic, add a custom tool. This example adds the gift_occasion_guide tool. The tool returns a suggested budget range and gift ideas for an occasion, so the agent does not guess them.
A tool is either research or customer-facing. The result of a research tool stays on the server, and the model uses it to write the answer. The result of a customer-facing tool also goes to the browser and renders as a UI block. The example tool is a research tool. For the customer-facing tools, see Storefront Assistant agents.
1) Add the tool plugin
The tool plugin implements Spryker\Client\AiFoundation\Dependency\Tools\ToolPluginInterface:
| METHOD | DESCRIPTION |
|---|---|
getName(): string |
Returns the unique tool name that the model calls. Use lowercase letters and underscores. |
getDescription(): string |
Returns the instruction for the model. It must say what the tool returns and when to call it. The model decides from this text whether to call the tool. |
getParameters(): array |
Returns the ToolParameterInterface list of the arguments that the model sends. Use SprykerFeature\Client\AiCommerce\StorefrontAssistant\Tool\ToolParameter, which takes the name, the ToolParameterType, a description, and a flag for a required argument. |
execute(...$arguments): mixed |
Runs the tool. The arguments are the values of the parameters, by name. Return an array. The array is encoded to JSON and sent to the model. |
src/Pyz/Client/AiCommerce/Plugin/StorefrontAssistant/Tool/GiftOccasionGuideToolPlugin.php
<?php
declare(strict_types = 1);
namespace Pyz\Client\AiCommerce\Plugin\StorefrontAssistant\Tool;
use Spryker\Client\AiFoundation\Dependency\Tools\ToolPluginInterface;
use Spryker\Client\Kernel\AbstractPlugin;
use Spryker\Shared\AiFoundation\Tools\ToolParameterType;
use SprykerFeature\Client\AiCommerce\StorefrontAssistant\Tool\ToolParameter;
class GiftOccasionGuideToolPlugin extends AbstractPlugin implements ToolPluginInterface
{
public const string TOOL_NAME = 'gift_occasion_guide';
protected const string TOOL_DESCRIPTION = <<<'DESCRIPTION'
Returns the suggested budget range in euros and gift ideas for one occasion. Call it when the customer names an occasion and has not given a budget, then use the result to search the catalog with `catalog_search`.
- `occasion` must be one of the listed values.
- If the result contains `error`, correct the request as the error says.
DESCRIPTION;
protected const string PARAMETER_OCCASION = 'occasion';
protected const array OCCASION_GUIDES = [
'birthday' => ['budgetMin' => 20, 'budgetMax' => 100, 'ideas' => ['camera', 'headphones', 'smartwatch']],
'wedding' => ['budgetMin' => 50, 'budgetMax' => 250, 'ideas' => ['tablet', 'speaker', 'photo frame']],
'housewarming' => ['budgetMin' => 15, 'budgetMax' => 60, 'ideas' => ['lamp', 'smart plug', 'speaker']],
];
/**
* {@inheritDoc}
*
* @api
*/
public function getName(): string
{
return static::TOOL_NAME;
}
/**
* {@inheritDoc}
*
* @api
*/
public function getDescription(): string
{
return static::TOOL_DESCRIPTION;
}
/**
* {@inheritDoc}
*
* @api
*
* @return array<\Spryker\Client\AiFoundation\Dependency\Tools\ToolParameterInterface>
*/
public function getParameters(): array
{
return [
new ToolParameter(
static::PARAMETER_OCCASION,
ToolParameterType::String,
sprintf('One of: %s.', implode(', ', array_keys(static::OCCASION_GUIDES))),
true,
),
];
}
/**
* {@inheritDoc}
*
* @api
*
* @param mixed ...$arguments
*
* @return array<string, mixed>
*/
public function execute(...$arguments): mixed
{
$occasion = (string)($arguments[static::PARAMETER_OCCASION] ?? '');
if (!isset(static::OCCASION_GUIDES[$occasion])) {
return ['error' => sprintf('Unknown occasion. Use one of: %s.', implode(', ', array_keys(static::OCCASION_GUIDES)))];
}
return static::OCCASION_GUIDES[$occasion];
}
}
Follow these rules in a tool:
- Do not throw an exception for an expected failure. Return an array with an
errorkey, and describe in the tool description how the model must react to it. - Return only data that the customer may see. The model can repeat any value of the result in its answer.
- To use a Client or a service, add the
create*()method to the projectAiCommerceFactoryand call it with$this->getFactory(), as the tools of the module do.
2) Register the tool
Add the plugin to the Client AiFoundationDependencyProvider::getAiToolPlugins(). Keep the tool plugins that are already there:
src/Pyz/Client/AiFoundation/AiFoundationDependencyProvider.php
use Pyz\Client\AiCommerce\Plugin\StorefrontAssistant\Tool\GiftOccasionGuideToolPlugin;
protected function getAiToolPlugins(): array
{
return [
// ... the tool plugins of Storefront Assistant
new GiftOccasionGuideToolPlugin(),
];
}
3) Add the tool to the agent
Add the tool name to the tool list of the agent in the Client AiCommerceConfig:
src/Pyz/Client/AiCommerce/AiCommerceConfig.php
public function getGiftAdvisorAgentToolNames(): array
{
return [
CatalogSearchToolPlugin::TOOL_NAME,
CategoryTreeToolPlugin::TOOL_NAME,
ProductDetailsToolPlugin::TOOL_NAME,
DisplayProductsToolPlugin::TOOL_NAME,
OfferChoicesToolPlugin::TOOL_NAME,
GiftOccasionGuideToolPlugin::TOOL_NAME,
];
}
A tool that is not in this list is not sent to the model, even if the plugin is registered. If the customer must see the result of the tool, also add the tool name to getStorefrontAssistantCustomerFacingToolNames() and add a frontend renderer. For details, see the tool renderer contract in vendor/spryker-feature/ai-commerce/src/SprykerFeature/Shared/AiCommerce/Theme/default/Chat/README.md.
4) Add the progress label
While the tool runs, the chat shows a progress label, such as “Searching the catalog”. Add the glossary key for each locale:
data/import/common/common/glossary.csv
>ai_commerce.storefront_assistant.tool.gift_occasion_guide,Looking up gift ideas,en_US
ai_commerce.storefront_assistant.tool.gift_occasion_guide,Geschenkideen werden gesucht,de_DE
Without the key, the progress label shows the raw glossary key.
5) Add the tool to the widget
The widget publishes the list of tool names to the frontend, which translates each name into the progress label. StorefrontAssistantWidget::addToolNamesParameter() contains a fixed list, so add the new name to it. Create a project widget that extends the module widget and overrides the protected method:
src/Pyz/Yves/AiCommerce/StorefrontAssistant/Widget/StorefrontAssistantWidget.php
<?php
declare(strict_types = 1);
namespace Pyz\Yves\AiCommerce\StorefrontAssistant\Widget;
use Pyz\Client\AiCommerce\Plugin\StorefrontAssistant\Tool\GiftOccasionGuideToolPlugin;
use SprykerFeature\Client\AiCommerce\Plugin\StorefrontAssistant\Tool\CatalogSearchToolPlugin;
use SprykerFeature\Yves\AiCommerce\StorefrontAssistant\Widget\StorefrontAssistantWidget as SprykerFeatureStorefrontAssistantWidget;
class StorefrontAssistantWidget extends SprykerFeatureStorefrontAssistantWidget
{
protected function addToolNamesParameter(): void
{
$this->addParameter(static::PARAMETER_TOOL_NAMES, [
CatalogSearchToolPlugin::TOOL_NAME,
// ... all other tool names of the module widget
GiftOccasionGuideToolPlugin::TOOL_NAME,
]);
}
}
Copy the full list of the tool names from addToolNamesParameter() of the module widget. Then register the project widget in ShopApplicationDependencyProvider::getGlobalWidgets() instead of the module widget, and update the widgetGlobalExists() and {% widget %} calls of your templates to use the same widget name.
6) Apply the changes
console cache:class-resolver:build
console data:import glossary
console queue:worker:start --stop-when-empty
console cache:empty-all
Make sure the following applies:
- Select Gift Advisor and send “I need a birthday gift”. The progress label “Looking up gift ideas” is displayed.
- The answer uses the budget range and the ideas of the
birthdayentry. - Send “I need a gift for a graduation”. The tool returns the
errorvalue, and the agent asks which of the listed occasions fits.
Thank you!
For submitting the form