In this blog, I experiment with Embabel Domain Injected Context Engineering (DICE). Dice can be used to manage extracted user preferences and use them to enrich your LLM context. I built upon the sample from my previous blog where I introduced the Embabel ChatBot.
Embabel DICE #
This example is heavily inspired by the example provided by the Embabel team, Impromptu. The example includes an extensive README; the DICE source is available. More documentation on DICE is also available.
A big problem when talking to an LLM, which most agents do, is the grounding gap. This is the lack of common ground between the LLM and the user’s question. The LLM generally has no idea what you are talking about. Imagine I have a conversation with the LLM in which I tell it what I like. I tell it I like to read blogposts about Agents and Embabel specifically. We had a nice chat, but I need to end the conversation.
Later that day, I go home and ask the agent or the LLM if there are any interesting new blog posts to read (I’m travelling by train today). By providing my preferences to the LLM, it knows that posts about Embabel should be at the top. Of course, this is a very simple scenario. But what if I can add domain-specific knowledge to a request I make? Think about allergies when ordering food, and your home address when ordering groceries. You name it.
When automating our daily work with agents, they need to understand what we are doing. That is where Domain Injected Context Engineering comes in.
In this blog, I create an example that is not too complex to understand using DICE and Embabel, along with a GUI to follow what the agent is doing.
GUM, the theory backing DICE #
As mentioned in the documentation, DICE is heavily inspired by the paper that introduces General User Models (GUM). The paper is good to read.
Creating General User Models from Computer Use
Omar Shaikh, Shardul Sapkota, Shan Rizvi, Eric Horvitz, Joon Sung Park, Diyi Yang, Michael S. Bernstein
I do not want to give a full summary of the article; read it for yourself. But I want to describe some of the highlights for me.
The idea from GUM is to learn from user interactions with a computer. If conversations have common ground among the involved parties, they just run better. If the system can learn about the user, it can create that common ground.
By observing, the system creates propositions on what a user wants to accomplish. The system also gives a confidence factor about the accuracy of the proposition. After new observations, the system can revise some of the propositions. The system also audits the propositions for facts you do not want the system to remember.
I also liked the staleness factor of a proposition. Some memories you want to keep longer than others. This factor is part of the proposition extracted by the system.
The steps #
In this section, I give you an overview of what we will do. I’ll explain the different steps, so you have a better idea of what is coming.
Have an Agent capable of talking to you #
This is where I left off in the previous blog. A ChatBot with RAG, making use of Lucene to index blogs and expose it as a tool to the ChatBot. Based on the impromptu sample mentioned, I made some improvements before adding the new preference extraction. The biggest change is that the user now stores the processId; therefore, the client application no longer needs to pass it to the backend.
Attaching the user to the agent process is now performed in an Embabel action. Note how the user is obtained from the OperationContext.
@Action
public KnowledgeUser bindUser(OperationContext context) {
var forUser = context.getProcessContext().getProcessOptions().getIdentities().getForUser();
if (forUser instanceof KnowledgeUser iu) {
return iu;
} else {
logger.warn("bindUser: forUser is not an KnowledgeUser: {}", forUser);
return null;
}
}This KnowledgeUser is injected into the action that responds to a UserMessage.
@Action(canRerun = true, trigger = UserMessage.class)
public void respond(Conversation conversation, KnowledgeUser user, ActionContext context) {
var lastUserMessage = conversation.lastMessageIfBeFromUser();
if (lastUserMessage != null) {
logger.info("Received user message as last message: {}", lastUserMessage.getContent());
var assistantMessage = context.ai()
.withLlmByRole(CHEAPEST.name())
.withReferences(toolishRag)
.withSystemPrompt("You are a helpful assistant. Answer questions concisely. Always address the current user by their name: " + user.getDisplayName() + ".")
.respond(conversation.getMessages());
context.sendMessage(conversation.addMessage(assistantMessage));
eventPublisher.publishEvent(new ConversationAnalysisRequestEvent(this, user, conversation));
} else {
logger.info("Received non-user message");
}
}If you read my previous post, you can see another difference. In the code, I publish an event. This event, ConversationAnalysisRequestEvent, is essential for integrating the conversation with preference extraction. I get back to this event in a later section.
Have a preference extraction tool #
The preference extraction tool receives some text and extracts the propositions for a preference from it. If I enter this text:
I like to watch tutorials about Agents, especially if the speaker uses a bit of humour but clearly knows what he is talking about.
The resulting propositions contain the extracted preferences, the confidence that the preference is right, and the explanation of why the preference was proposed. The first proposition is: User likes to watch tutorials about Agents. The extractor has a confidence of 90% and gives the following reason: User explicitly says they like to watch tutorials about Agents.
The image below shows the tool I created to work with the extractor tool. In Embabel, this part is called the PropositionPipeline. The extraction is exposed by the PropositionPipelineController.

The proposition extractor test component. Extracts propositions from the provided text.
Another important piece of information for a proposition is the decay. A high decay rate means the proposition’s importance decreases rapidly over time. Low decay indicates the proposition remains an important preference for the user. Think about the difference between where you parked your car and the day you got married.
The following diagram gives an overview of the Embabel design for the proposition pipeline. The PropositionPipelineController is a Spring MVC Rest controller that exposes an endpoint for extracting propositions from the provided text. The MemoryController has endpoints for listing and searching the stored propositions, i.e., the memory. It also provides endpoints for creating and deleting propositions.
The PropositionPipeline orchestrates the proposition extraction. It uses the extractor to find propositions, and the revisor to determine whether we are dealing with a new proposition or an update to an existing one. Propositions are stored or updated in the repository.

Design for the proposition pipeline
The following screenshot shows some propositions that were extracted from a conversation I had with the ChatBot. Notice the mentions in the last item. Next to the Knowledge user, there is another entity called Product. In another post, I take a better look at entity extraction. The impromptu sample contains more entities and relations, which are stored in a graph database.

Four propositions extracted from a conversation with options to refresh the list and delete a specific proposition.
In the following section, I discuss integrating the PropositionPipeline with the Conversation and the ChatBot.
Integrate the extraction tool with the conversation #
Now that I have a pipeline to extract preferences from text, I want to use this to extract user preferences from the conversation. The extraction begins by listening to an event I discussed a few sections back. After the user responds to a request, the conversation is published as an event.
The ConversationAnalysisRequestEvent contains the conversation and the KnowledgeUser. I wrote the ConversationPropositionExtraction class using the impromptu sample code. This class listens for the event and uses several components to extract entities and propositions from the conversation. The entities to extract are defined in the DataDictionary. With the help of the ChunkHistoryStore, re-analysing the same chunk is prevented. After extraction, the propositions and entities are stored in the PropositionRepository and the EntityRepository.
Internally, the PropositionPipeline discussed in the previous section is used for the extraction of the propositions. The following diagram gives an overview of the discussed components.
Shows how the events is obtained and the propositions are extracted and stored.
With user preferences extracted as propositions from the conversation, we only need to use the preferences in the context of the agent.
Inject user preferences into the context of the Agent #
Propositions are exposed through a memory component. The Memory needs the PropositionRepository and a MemoryProjector. This projector translates proposals as semantic memory (facts), procedural memory (preferences/rules) or episodic memory (events).
Next, the memory is exposed as a reference to the LLM call. The following code block shows the same function as before, now with the included memory.
@Action(canRerun = true, trigger = UserMessage.class)
public void respond(Conversation conversation, KnowledgeUser user, ActionContext context) {
var lastUserMessage = conversation.lastMessageIfBeFromUser();
if (lastUserMessage != null) {
var memory = Memory.forContext(user.getCurrentContext())
.withRepository(propositionRepository)
.withProjector(memoryProjector);
logger.info("Received user message as last message: {}", lastUserMessage.getContent());
var assistantMessage = context.ai()
.withLlmByRole(CHEAPEST.name())
.withReferences(toolishRag, memory)
.withSystemPrompt("You are a helpful assistant. Answer questions concisely. Always address the current user by their name: " + user.getDisplayName() + ".")
.respond(conversation.getMessages());
context.sendMessage(conversation.addMessage(assistantMessage));
eventPublisher.publishEvent(new ConversationAnalysisRequestEvent(this, user, conversation));
} else {
logger.info("Received non-user message");
}
}More code #
In the previous section, you read about the concepts and were introduced to the design of the Embabel ChatBot with DICE. You can now read the sample in the git repository. I always like to see the code with some explanation; therefore, I go over the same steps, but now show the code.
Maven #
For some features, I needed the bleeding edge. At the time of writing, I am using version 0.3.3-SNAPSHOT for embabel. (0.3.3–20260126.051307–64) I will update the sample when a release becomes available that contains the things I need.
Therfore you do need to add the snapshot repo from Embabel to your pom.
<repositories>
<repository>
<id>embabel-snapshots</id>
<url>https://repo.embabel.com/artifactory/libs-snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>DICE is a separate module; we need that dependency as well.
<dependency>
<groupId>com.embabel.dice</groupId>
<artifactId>dice</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>Again, a snapshot dependency, 0.1.0–20260125.055345–4, to be specific.
The code for the PropositionPipeline #
Most of the code is Spring configuration code. So a lot of beans. I start with the bean for the proposition repository.
@Bean
PropositionPipeline propositionPipeline(
PropositionExtractor propositionExtractor,
PropositionReviser propositionReviser,
PropositionRepository propositionRepository) {
return PropositionPipeline.withExtractor(propositionExtractor)
.withRevision(propositionReviser, propositionRepository);
}Just like in the design diagram you have seen in a previous section. The pipeline has three dependencies. It needs an extractor, a reviser and a repository.
First, the repository needs the model provider to select an Embedder. I like to select it by role. This is configured in the application.yml and an enum. I use the InMemory version of the repository. It only needs an embedding model to create the vectors for similarity search.
@Bean
PropositionRepository propositionRepository(ModelProvider modelProvider) {
return new InMemoryPropositionRepository(
modelProvider.getEmbeddingService(ByRoleModelSelectionCriteria.Companion.byRole(FAST.name()))
);
}Next, we need the reviser. The reviser uses an LLM to find existing revisions of a proposition.
@Bean
PropositionReviser propositionReviser(AiBuilder aiBuilder) {
var ai = aiBuilder
.withShowPrompts(true)
.withShowLlmResponses(true)
.ai();
return LlmPropositionReviser
.withLlm(LlmOptions.withLlmForRole(STANDARD.name()))
.withAi(ai);
}Notice that we tell Embabel to show the prompt and the LLM response. In the impromptu example, these properties are extracted from a Properties object to make them configurable. That is better, but this is easier for now.
Finally, you need the extractor. This is a bigger one, follows the same structure as the reviser, but with a big twist.
@Bean
LlmPropositionExtractor llmPropositionExtractor(
AiBuilder aiBuilder,
PropositionRepository propositionRepository) {
var ai = aiBuilder
.withShowPrompts(true)
.withShowLlmResponses(true)
.ai();
return LlmPropositionExtractor
.withLlm(LlmOptions.withLlmForRole(STANDARD.name()))
.withAi(ai)
.withPropositionRepository(propositionRepository)
.withSchemaAdherence(SchemaAdherence.DEFAULT)
.withTemplate("extract_user_propositions");
}Notice that the code uses a template for the LLM prompt. Again copied at first from the impromptu sample. But made it work for my sample. It is a long one, but I think it is worth a look.
You are extracting propositions about blog writers and readers
from their conversation.
Extract facts about the USER only -
their preferences, knowledge, interests, and opinions about blogs and technology.
{% include "dice/schema_hints.jinja" %}
{% include "dice/existing_propositions.jinja" %}
## Extraction Rules
1. **User-centric**: Extract facts about the user, not general blogging knowledge
2. **Single fact per proposition**: One subject, one object maximum
3. **No inference beyond evidence**: Only extract what the text explicitly supports
4. **No overlap**: Each fact should be distinct
## Examples
User says "I want to read blogs about Embabel":
✓ GOOD:
- "Jettro likes to read blogs" (NO entity mention - period/style)
- "Jettro likes blogs about Embabel" (entity: Embabel → Product)
- "Jettro programs best in Python and Java" (entity: Java -> ProgrammingLanguage)
## Confidence & Decay
- **High confidence**: Explicit statements ("I love reading about AI")
- **Lower confidence**: Implied preferences (asking many questions about a topic)
- **Low decay**: Stable preferences (favorite topic, category preferences)
- **High decay**: Transient states (current writing topic)
## Input
User: {{ user.name }}
<conversation>
{{ model.chunk.text }}
</conversation>Notice how the template includes templates provided by DICE. The template contains the extraction rules, examples and explains the concept of confidence and decay.
With this configuration, the pipeline is usable by the provided PropositionPipelineController to request propositions for a given piece of text.
The code for extracting propositions from the conversation #
Next up, the class that listens for *ConversationAnalysisRequestEvent *and handles them by starting the extraction process. This is the ConversationPropositionExtraction, which contains most of the required code to work with propositions.
The class needs an entity resolver, an entity repository, a proposition pipeline, a proposition repository, a data dictionary and a chunk history store. The proposition pipeline and repository are already available.
First, the data dictionary. This contains the entities that can be extracted and used in the propositions. At this moment, I have the KnowledgeUser, the Product and the ProgrammingLanguage.
@Bean
@Primary
DataDictionary blogDataDictionary() {
var schema = DataDictionary.fromClasses("blog",
Product.class,
ProgrammingLanguage.class,
KnowledgeUser.class
);
logger.info("Initialized data dictionary with classes: Product, ProgrammingLanguage");
return schema;
}This DataDictionary is used by the SchemaRegistry; in my case, again, an InMemory version.
@Bean
SchemaRegistry schemaRegistry(DataDictionary blogDataDictionary) {
var registry = new InMemorySchemaRegistry(blogDataDictionary);
logger.info("Initialized schema registry with classes: Product, ProgrammingLanguage");
return registry;
}All the other required beans are InMemory implementations.
@Bean
EntityResolver entityResolver() {
return new InMemoryEntityResolver();
}
@Bean
ChunkHistoryStore chunkHistoryStore() {
return new InMemoryChunkHistoryStore();
}
@Bean
NamedEntityDataRepository namedEntityDataRepository(DataDictionary dataDictionary, ModelProvider modelProvider) {
return new InMemoryNamedEntityDataRepository(
dataDictionary,
modelProvider.getEmbeddingService(ByRoleModelSelectionCriteria.Companion.byRole(FAST.name()))
);
}The constructor of the ConversationPropositionExtraction initialises the IncrementalAnalyzer. This analyser is responsible for analysing the conversation to extract propositions, if required.
this.analyzer = new PropositionIncrementalAnalyzer<>(
propositionPipeline,
chunkHistoryStore,
MessageFormatter.INSTANCE,
config
);The following codeblock shows the extraction and persistence of the propositions. I removed some logging; check the actual source code if you want the complete picture.
public void extractPropositions(ConversationAnalysisRequestEvent event) {
logger.info("Starting proposition extraction for conversation with {} messages",
event.conversation.getMessages().size());
var messages = event.conversation.getMessages();
if (messages.size() < 2) {
logger.info("Not enough messages to extract propositions, need at least 2");
return;
}
var context = SourceAnalysisContext
.withContextId(event.user.getCurrentContext())
.withEntityResolver(entityResolverForUser(event.user))
.withSchema(dataDictionary)
.withKnownEntities(KnownEntity.asCurrentUser(event.user))
.withPromptVariables(Map.of(
"user", event.user
));
// Wrap conversation as incremental source and analyze
var source = new ConversationSource(event.conversation);
var result = analyzer.analyze(source, context);
if (result == null) {
logger.info("Analysis skipped (not ready or already processed)");
return;
}
if (result.getPropositions().isEmpty()) {
logger.info("Analysis completed but no propositions extracted");
return;
}
result.persist(propositionRepository, entityRepository);
}The last line adds the new entities and propositions to their respective repositories.
The code to use the propositions #
I already discussed this code in the previous section, when I discussed the changes to the action to add the memory as a resource to the LLM call. The only thing missing is the memory projector’s config.
@Bean
MemoryProjector memoryProjector() {
return DefaultMemoryProjector.DEFAULT;
}Running the application #
One thing I really like about Embabel is the logging. You can follow everything from the logs. With the trace of Agent events, you also track LLM calls, tool calls, and, of course, their responses.
First question:
I am really interested in blogs about Embabel. Do you have information about such blogs?
In the log, you can find this line. Notice how “I” is extracted as a KnowledgeUser.
20:32:05.318 [tomcat-handler-16] INFO ConversationPropositionExtraction - ChunkPropositionResult(chunk=423b39ea-0004–4a5f-8873–1317c44fe2a4, propositions=2, entities: 1 new, 0 updated, revision: 2 new, 0 merged, 0 reinforced, 0 contradicted, 0 generalized)
Propositions:
• Jettro is really interested in blogs about Embabel (conf: 0.98) [NEW]
• Jettro asked for information about blogs about Embabel (conf: 0.95) [NEW]
Entities:
• [NEW] I (KnowledgeUser, __Entity__)After some questions and answers, go to the propositions tab to find what propositions are extracted. I removed a few to make the list easier to read. I personally like the middle one, where a proposition is extracted from the assistant’s response.

Some extracted propositions from a conversation between me and the agent about Embabel.
References #
That is it. I hope you learned something from this blog. I know I learned a lot while writing the blog and creating the sample. I keep adding features to the sample. I try to keep track of the latest and greatest Embabel features.
Drop me a comment if you liked it or if you have a question.
- Blogs about Embabel: https://medium.com/embabel
- Impromptu sample: https://github.com/embabel/impromptu
- Embabel homepage: https://github.com/embabel/embabel-agent
- DICE: https://github.com/embabel/dice
- This sample: https://github.com/jettro/embabel-agent-rag-sample
Agents that extract and use preferences from conversations. was originally published in Embabel on Medium, where people are continuing the conversation by highlighting and responding to this story.