AI Module Extensibility Guide
The AI module in Bloomreach Content supports extensibility through several integration points. You can provide custom implementations for the following components:
- Model Providers
- Vector Stores (experimental)
- Tool Packages
Model Providers
Implement the com.bloomreach.xm.ai.service.api.client.AIModelFactory interface to integrate a custom org.springframework.ai.chat.model.ChatModel or org.springframework.ai.embedding.EmbeddingModel.
public interface AIModelFactory<C extends ChatModel, E extends EmbeddingModel> { String getModelProviderName(); boolean isEnabled(); List<ModelFeature> getModelFeatures(); C createChatModel(); E createEmbeddingModel(); }
The AI backend discovers and injects model provider factories dynamically using Spring Dependency Injection. To enable this, annotate your implementation with @Component or @Service and ensure it is available on the classpath.
To access configuration from both JCR and properties files, extend the abstract class com.bloomreach.xm.ai.service.impl.client.chatmodel.JcrAIModelFactory. The following example shows part of an Ollama provider implementation:
@Component public class OllamaModelFactory extends JcrAIModelFactory<OllamaChatModel, OllamaEmbeddingModel> { public OllamaModelFactory(final JcrFirstConfiguration jcrFirstConfiguration) { super(jcrFirstConfiguration); if (isEnabled()) { List.of(SPRING_AI_OLLAMA_API_URL_PROP_NAME, ... SPRING_AI_OLLAMA_PULL_STRATEGY_PROP_NAME) .forEach(this::ensureRequiredPropertyConfigured); } if (isEnabled()) { log.info("ChatModelFactory [{}] active", getModelProviderName()); } } @Override public String getModelProviderName() { return "Ollama"; } @Override public OllamaChatModel createChatModel() { if (!isEnabled()) { throw new IllegalStateException("Ollama Model is not enabled"); } return OllamaChatModel.builder() .ollamaApi(OllamaApi.builder() .baseUrl(getProperty(SPRING_AI_OLLAMA_API_URL_PROP_NAME)).build()) .defaultOptions(OllamaChatOptions.builder() .model(getProperty(SPRING_AI_OLLAMA_CHAT_OPTIONS_MODEL_PROP_NAME)) .build()) ... .build(); } @Override public OllamaEmbeddingModel createEmbeddingModel() { if (!isEnabled()) { throw new IllegalStateException("Ollama Model is not enabled"); } final OllamaEmbeddingOptions.Builder optionsBuilder = OllamaEmbeddingOptions.builder(); if (isConfigured(SPRING_AI_OLLAMA_EMBEDDING_OPTIONS_MODEL_PROP_NAME)) { optionsBuilder.model(getProperty(SPRING_AI_OLLAMA_EMBEDDING_OPTIONS_MODEL_PROP_NAME)); } return new OllamaEmbeddingModel( OllamaApi.builder().baseUrl(getProperty(SPRING_AI_OLLAMA_API_URL_PROP_NAME)).build(), optionsBuilder.build(), ObservationRegistry.NOOP, ... }
Vector Stores
To use a custom vector store, implement the com.bloomreach.xm.ai.service.api.client.VectorStoreFactory interface. This allows you to provide a different org.springframework.ai.vectorstore.VectorStore implementation.
public interface VectorStoreFactory<V extends VectorStore> { String getVectorStoreName(); boolean isEnabled(); V createVectorStore(); }
The AI backend discovers and injects vector store factories dynamically using Spring Dependency Injection. Annotate your implementation with @Component or @Service and ensure it is available on the classpath.
To access configuration from both JCR and properties files, extend the abstract class com.bloomreach.xm.ai.service.impl.vector.JcrVectorStoreFactory. The following example shows part of a Redis vector store factory:
@Component public class RedisVectorStoreFactory extends JcrVectorStoreFactory<RedisVectorStore> { public RedisVectorStoreFactory(final JcrFirstConfiguration jcrFirstConfiguration, final ObjectProvider<EmbeddingModel> embeddingModel) { super(jcrFirstConfiguration); this.embeddingModel = embeddingModel.getIfAvailable(); if (this.embeddingModel == null) { log.warn("EmbeddingModel should not be null, disabling store"); disabled = true; } if (isEnabled()) { List.of(REDIS_HOST_PROP_NAME, REDIS_PORT_PROP_NAME, ... .forEach(this::ensureRequiredPropertyConfigured); } if (isEnabled()) { log.info("VectorStoreFactory [{}] active", getVectorStoreName()); } } @Override public String getVectorStoreName() { return "Redis"; } @Override public RedisVectorStore createVectorStore() { if (!isEnabled()) { throw new IllegalStateException("Redis Vector Store is not enabled"); } return RedisVectorStore.builder(...) .indexName(getProperty(REDIS_INDEX_PROP_NAME)) .prefix(getProperty(REDIS_PREFIX_PROP_NAME)) ... .build(); } }
Tool Packages
Implement the com.bloomreach.xm.ai.service.api.client.ClientToolPackage interface to register custom tools that the AI module can use. A tool package can contain multiple tools.
public interface ClientToolPackage { boolean isEnabled(); String getAdvice(); Object getTools(); }
Prompt Instructions
The getAdvice() method allows you to provide natural language instructions about your tools. These instructions are appended to the system prompt to help the AI understand how to use the tools. You can also provide instructions in the tool descriptions using the @Tool annotation. If you do not want to provide additional advice, return an empty string.
Registration
The AI backend discovers and injects tool packages dynamically using Spring Dependency Injection. Annotate your tool package with @Component or @Service and ensure it is available on the classpath. This enables you to build and register custom tools for your project.
Capabilities
All Spring beans from the AI module are available to your tools. For example, to access the Vector Store, add ObjectProvider<VectorStore> vectorStore as a constructor argument.
Tools are executed during a user's chat session in the following sequence:
User chats → AI receives request → AI calls tool → Tool executes and responds to AI → AI responds to user
The execution order of tools can be controlled using the @Order annotation, for example @Order(10).
Tools receive a "context" map containing attributes set by the AI backend. Use the helper methods in com.bloomreach.xm.ai.service.impl.client.advisors.AdvisorContextUtils to access context items:
static <T> Optional<T> getFromContext(final Map<String, Object> context, final String key, final Class<T> type) static Optional<String> getStringFromContext(final Map<String, Object> context, final String key)
Available context attributes include:
userId: The username of the user interacting with the AI (String)activeContextItem: The current open document (com.bloomreach.xm.ai.repository.services.content.model.Document) the user is working on
Tools should execute quickly and block until they return a response, as the AI and user wait for the result.
Example Tool Package
The following example shows a tool package that provides two tools: fetchUrlTool and searchBloomreach.
Info: These examples are for demonstration purposes only and are not suitable for large HTML responses.
@Component @Order(10) public final class ExternalUrlToolPackage implements ClientToolPackage { public ExternalUrlToolPackage() { log.info("ExternalUrlToolPackage is enabled"); } @Override public boolean isEnabled() { return true; } @Override public String getAdvice() { return """ If the user asks for the content of an external url, use the 'fetchUrlTool' tool passing the url as an argument. The tool will provide you with the html content of the webpage, then make a text-based summary of it and present it to the user. Use markdown if you think it can help preserving the formatting you see in the webpage's html. If the user is requesting to search the Bloomreach documentation for some information, convert this information into a space separated list of keywords and use tool 'searchBloomreach' passing that information as an argument. Then process the html result and present the results to the user. """; } @Override public Object getTools() { return new Object() { @Tool(name = "fetchUrlTool", description = "Fetch content of a webpage") Document fetchUrlTool( @ToolParam(description = "The url of the webpage to fetch content for") String url, ToolContext toolContext) { final HttpClient client = HttpClient.newHttpClient(); try { final String htmlResponse = client.send( HttpRequest.newBuilder() .uri(URI.create(url)) .GET() .build(), HttpResponse.BodyHandlers.ofString()) .body(); return Document.builder() .id(url) //TODO Html content is too large for the AI, use jsoup .text(htmlResponse) .metadata("url", url) .build(); } catch (IOException e) { return error("Failed to fetch content of webpage", e); } catch (InterruptedException e) { return error("Timed out while fetching content of webpage", e); } } @Tool(name = "searchBloomreach", description = "Perform a search for some keywords against the Bloomreach documentation site") //TODO Should return List<Document> instead of asking the AI to process raw html Document searchBloomreach( @ToolParam(description = "Keywords that must be used for the search") String keywords, ToolContext toolContext) { final String url = String.format("https://xmdocumentation.bloomreach.com/librarysearch?query=%s", URLEncoder.encode(keywords, StandardCharsets.UTF_8)); //TODO Process the html result and return List<Document> return fetchUrlTool(url, toolContext); } private static Document error(final String friendlyMessage, final Exception e) { log.error(friendlyMessage, e); return Document.builder() .text(String.format("Error, %s: %s", friendlyMessage, e.getMessage())) .build(); } }; } }