Filtering by Inclusion
Recipe Filtering
We need to refine our logic now for retrieving only those recipes that satisfy specific filters. We already introduced a recipeFiltered/3
predicate for Request a Recommendation but there it did not have to do any serious work yet. We only introduced the base case without any filters (an empty list of filters) but now need to deal with the case where we have some filters that a user provided. The basic idea to define a recursive clause for recipeFiltered/3
is simple: apply the first filter in the list to the recipes and recursively filter the remaining recipes using the remaining filters. We provide you below with the recursive clause that you should add below the base clause that we added earlier in the recipe_selection.pl
file:
% These functions should go in recipe_selection.pl some are already there.
recipesFiltered(RecipeIDs) :-
recipeIDs(RecipeIDsAll),
filters_from_memory(Filters),
recipesFiltered(RecipeIDsAll, Filters, RecipeIDsFiltered),
list_to_set(RecipeIDsFiltered, RecipeIDs).
% Recursively go through all user provided features to find those recipes that satisfy all
% of these features.
recipesFiltered(RecipeIDs, [], RecipeIDs).
recipesFiltered(RecipeIDsIn, [ ParamName = Value | Filters], RemainingRecipeIDs) :-
applyFilterCheck(ParamName, Value, RecipeIDsIn, RecipeIDsOut),
recipesFiltered(RecipeIDsOut, Filters, RemainingRecipeIDs).
applyFilterCheck(ParamName, Value, RecipeIDsIn, RecipeIDsOut) :-
is_list(Value), [H | T] = Value,
applyFilter(ParamName, H, RecipeIDsIn, RecipeIDsOut),
applyFilterCheck(ParamName, T, RecipeIDsIn, RecipeIDsOut).
applyFilterCheck(_, [], RecipeIDsIn, RecipeIDsIn).
applyFilterCheck(ParamName, Value, RecipeIDsIn, RecipeIDsOut) :-
not(is_list(Value)),
applyFilter(ParamName, Value, RecipeIDsIn, RecipeIDsOut).
Inspect this rule carefully and make sure that you understand what it is doing.
Checking that a recipe uses an ingredient
If we want to check that a recipe uses an ingredient, we need to be able to check whether the ingredient is included in the ingredient list of that recipe. For ingredient types, things are slightly more complicated. To do both checks, we will add two rules for defining the hasIngredient(RecipeID, Ingr)
predicate that succeed if the recipe with identifier RecipeID
uses the ingredient (type) Ingr
. Both rules need to be added to the ingredient_hiearchy.pl
file (at a location indicated in that file).
The first Prolog rule for hasIngredient
is designed to determine if a specific ingredient is used in a given recipe. It simply checks if Ingr
is included in the ingredient list of recipe RecipeID
by using the ingredient/2
predicate (see the recipe_database.pl
file). The second rule is designed to determine if a recipe uses an ingredient type. It assumes that Ingr
is a type of ingredient (such as meat). To check whether a recipe uses that ingredient type, however, we need to find a specific ingredient such as steak that is an example instance of the type (only those are directly associated with a recipe in the recipe database). Another example would be apple as an instance of the type fruit. We do not have to specify all of these relations as a lot of the work has already done for you and you can use the typeIngredient/2
predicate in the ingredient_hiearchy.pl
file (check out the typeIngredient/2
facts in that file). Now use that predicate in combination with the ingredient/2
predicate to define the second rule. Add your rules to the ingredient_hierarchies.pl
file.
Applying filters: Ingredient, meal type, and cuisine
After you add the recursive clause for recipesFiltered/3
above, you will see that Eclipse will start to complain that the applyFilter/4
predicate has not been defined. We have reduced the problem of filtering recipes to the problem of applying a single filter to a set of (remaining) recipes. The idea is that the applyFilter(+ParamName, +Value, +RecipeIDs, -FilteredRecipes)
filters the RecipeIDs
provided as input by means of the feature ParamName
using a specific Value
and returns the recipes that satisfy this feature in the output argument FilteredRecipes
. The parameter name ParamName
refers to a feature that a recipe should have, for example, it should be of a certain cuisine. The specific value Value
of the recipe feature specifies what exact feature request is made, for example, the cuisine should be Chinese. For each feature, we need to write a separate applyFilter/4
rule. For now, we need to provide a rule for the ‘ingredient'
feature (use the same name for the feature as the name of the corresponding parameter name used in the addFilter intent in your intent and slot classifier), and the 'cuisine'
feature. The body of the first rule can be designed with the hasIngredient/2
predicate. You must find all (Hint for what Prolog built-in predicate you should use) recipes from the list that you start with that satisfy the filter and return these recipes in the output argument list. Add your rules to the recipe_selection.pl
file at the location indicated in the file; there you will also find the heads of the rules that you need to define. The rule for filtering on cuisine is very similar too but instead of the hasIngredient/2
predicate you should use the cuisine/2
predicate (see the recipe_database.pl
for examples). Lastly, the same can be done for mealType/2.
Applying filters: dietary restriction
When you inspect the recipe_database.pl
file, you will see that there are also no facts in the database that suggest that a recipe is vegan or spicy, for example (two restrictions that are included in the dietaryRestrction entity in the Dialogflow agent). As for the ingredientType entity, we need to add some logic to enable the agent to conclude from the basic knowledge stored in the database about a recipe that is satisfies a dietary restriction. We will use the typeIngredient/2
predicate for this too, again.
Filtering on dietary restrictions
The basic idea to implement the logic for checking a dietary restriction is to check whether each ingredient of a recipe meets that dietary restriction. It is clear, for example, that a recipe that uses chicken, is not a vegetarian recipe. The reasoning is simple: Vegetarians do not eat any kind of meat, chicken is a kind of meat, so any recipe that uses chicken is not a vegetarian recipe. If we can retrieve this information about chicken somehow, then we can implement this logic for our agent too. When you inspect the ingredient_hierarchies.pl
file, you will see that it includes facts that say exactly that. We find, for example, typeIngredient('chicken drumsticks', 'meat')
. We can use these facts to conclude that various kinds of chicken and other ingredients are meat. There are no facts, however, to conclude that an ingredient fits in a vegetarian or vegan diet. Instead, you will find facts indicating that certain ingredients are non-vegetarian and non-vegan. Of course, assuming that these lists of facts are complete (we give no guarantees, you might want to check that), we can then also conclude that all ingredients that are not explicitly listed as non-vegetarian should be vegetarian. Let’s first add some rules that implement this logic so that the agent can conclude that an ingredient is vegetarian, too. In the ingredient_hierarchies.pl
file, at the designated location, add, for example:
typeIngredient(Ingredient, 'vegetarian') :-
not(typeIngredient(Ingredient, 'non-vegetarian')), !.
Also add rules for the pescatarian and vegan dietary restriction. That enables our agent to handle at least three of the six dietary restrictions that we added to the dietaryRestriction entity of the Dialogflow agent. We will get back to the other three later. For now, you should assume that we can use the predicate typeIngredient/2
to check if an ingredient meets a dietary restriction. We want to implement a rule now that checks that all the ingredients in a list meet a dietary restriction, such as 'meat'
or 'vegan'
. You should implement an ingredientsMeetDiet/2
predicate in the recipe_selection.pl
file that for any given list of ingredients checks that each one of these ingredients meets a dietary restriction. In other words, write code for the body of the recursive clause for the following rule:
ingredientsMeetDiet([ Ingredient | Rest ], DietaryRestriction) :-
Of course, you will also need to add a base clause for this predicate with the empty list as first argument. Should this base clause succeed or fail for a dietary restriction?
Next, we introduce one more helper predicate to check that a recipe meets a dietary restriction. The idea is to find all the ingredients of a certain recipe with identifier RecipeID
and feed the resulting list in the ingredientsMeetDiet/2
predicate that we just defined to check that all these ingredients satisfy a DietaryRestriction
. In the recipe_selection.pl
file, add code for the body of the following rule:
Finally, using the diet/2
predicate, we can define a rule for applying a dietary restriction filter. You are asked to define applyFilter('dietaryrestriction', Value, RecipeIDsIn, RecipeIDsOut)
, similar to how rules for other filters have been defined.
Extending the logic of ingredient hierarchies
As we mentioned above, there is still information missing to implement the logic for all dietary restrictions, and also for food categories (you were already asked to add the pasta food category, but what about noodles, for example?). For the dietary restrictions, you should add typeIngredient/2
facts that can facilitate the processing of filters for lactose-free (no dairy), gluten-free (no gluten), and spicy recipes. For all three, use a simple approach that only looks at the individual ingredients that are used in a recipe. That is, for a lactose-free recipe, no dairy products should be used; for a gluten-free recipe, no products with gluten should be used; and, for a spicy recipe, the recipe should have (at least) one ingredient that is spicy (the hasIngredient/2
predicate should come to mind here). As before, we can use one fact to deduce the other, and, deduce, for example, that if an ingredient has no gluten it must be gluten-free. We can define such relations using the same type of rule as we have seen above:
This rule, however, still assumes that the typeIngredient(Ingredient, ‘gluten')
facts are given somehow. We provide an example of what that should look like for the 'gluten'
facts:
For the lactose-free and spicy features, you should add the relevant facts. We recommend using information that you can find online and tools to automate your approach to compiling these lists.
Even though some of the recipe features that we have looked at require careful analysis, those that require more work than simply inspecting basic facts in the recipe database were all based on reasoning about individual ingredients. That is, we verified whether an ingredient is of a particular type, either a type of food or a type of dietary restriction. If you like a challenge, you can also consider other recipe features that consider more global features of a recipe, such as a recipe being low-carb or cheap. How would you define the logic for such filters? What kind of information would you need to add to the database to be able to define features like these?
Dialogue Patterns
Requesting a recipe feature
Looking ahead, we would like to be able to conduct conversational interactions like the following example:
A: What kind of recipe are you thinking about?
U: I’d like a recipe from Japan.
A: The remaining recipes are of Japanese cuisine. Is there anything else you'd like?
U: Recipes that use salt.
A: All the recipes left include salt. Do you want to add another preference?
U: Teriyaki salmon.
A: Teriyaki salmon is a great choice!
A: Can you check the recipe and let me know if you want to cook this?
Let’s inspect this interaction in more detail and analyse how it is organized. In Moore and Arar’s taxonomy, the parts of the dialog in this example where the user adds a feature request in the second and fourth move can be classified as Pattern A2.1: Open Request. We will use the a21featureRequest
label for this pattern and define different variants of it below. The agent initiates with an inquiry about what the user is looking for. This move is part of the a50recipeSelect
pattern, a top level pattern that we added to the agent’s agenda. We previously defined variants for that pattern for Select Recipes by Name . The user responds by informing the agent that they are interested in a particular cuisine (Japanese). This is a feature request that we want the agent to manage by means of a new a21featureRequest
pattern. The idea is that this pattern is inserted while the user and agent are still performing the top level a50recipeSelect
pattern. The a21featureRequest
pattern is inserted as a subdialog, which we indicated in the example above by adding indentation. The response of the agent is an acknowledgement move combined with a renewal of the question whether the user wants to add any other feature. We’ll take it that this move is also part of the a21featureRequest
pattern and ends this pattern. The fourth user move (a second feature request for the an ingredient type pasta) and the fifth agent move repeats the same a21featureRequest
pattern. The interaction concludes with a move of the user mentioning a specific recipe title (Teriyaki salmon). That last move and the agent move following it are both part of the a50recipeSelect
pattern we specified for Select Recipes by Name . That pattern added the a50recipeConfirm
pattern into the current session of the agent. This pattern asks the user to check the recipe (while showing all relevant details). You still need to add this pattern to implement the capability we are working on right now below.
From a visual (webpage) point of view, we want to inform the user about the progress that is made. The main difference that we think is relevant here is how many recipes are still remaining. That is, how many recipes satisfy all of the user’s requests. The idea here is that it is not useful to show titles and pictures of a large number of recipes (there would be too many for the user to look at) but to show titles and pictures when the number of remaining recipes has reduced to a sufficiently small number (as a design choice, we have chosen <16 here). Assuming that there are fewer than 16 recipes left that are Japanese and use salt, at the sixth move in the example above, the user can have a look at the displayed recipe titles that are still left and make a choice. That should also clarify how at that move the user can mention a specific recipe title (they see the titles that are left). After selecting a recipe, the agent acknowledges the choice and asks the user to confirm they would like to cook the recipe. At this stage, the conversational context is the a50recipeConfirm
pattern and the agent should display all relevant details such as instructions and ingredients that help a user make this decision on the corresponding page.
There is still one important design choice that we need to make at this stage. The choice is whether we want a user to still be able to make feature requests when they are already checking a specific recipe. That is, when the top level conversational context is the a50recipeConfirm
pattern and a corresponding page is showing a specific recipe title, picture and other details, should a user still be able to add or remove feature requests? We will proceed with the design choice that a user should be able to still make feature requests when already checking a recipe (but you should feel free to change that if your team disagrees with this choice; you would end up with a different set of patterns than the ones we present below in that case). The consequence of our choice from a conversational point of view is that when either the a50recipeSelect
or the a50recipeConfirm
pattern are the top level patterns, we should enable the insertion of a feature request pattern as a subdialog in the conversation. Taking our design choice into account, we end up with 2 main variants of the feature request pattern:
A variant for when a user requests a feature while already checking a recipe and the context is
a50recipeConfirm
. That is, the user already has made a choice (e.g., Teriyaki salmon), is looking at the recipe details for this choice , but still adds a feature request. We assume that the user is still undecided if they make such a move, and will want to move the conversation back to thea50recipeSelect
pattern stage by inserting that pattern back into the session at the end of the patterna21featureRequest
.A variant for when a user requests a feature while the context still is selecting a recipe (
a50recipeSelect
).
Concluding, either way, when a user makes a feature request, we make a design choice to move the conversation to go back to (option 1) or stay (option 2) in the recipe selection stage (i.e., the a50recipeSelect
top level context). Implicitly, we are also saying here that making a feature request dialog move is an out of context intent (see Handle Unexpected Intents ) when the conversation is neither in the recipe selection nor confirmation stage.
Apart from this design choice, there are a few other aspects of the filtering process that we should take into account. One aspect that we need to consider is whether the user requests are consistent or not. If a new user request conflicts with some of the features that have been requested before, we want the agent to remove those that are conflicting with the new request(s). Here we assume that newer requests should override the older ones. A second important aspect is the distinction between the case where after adding a new filter there are still recipes that satisfy all feature requests versus the case where there are no recipes left that meet all requests. For the latter case, we want the agent to respond differently and instead of an acknowledgement move make a dialog move that informs the user that there is no recipe that meets all feature requests.
Implementing the a21featureRequest pattern variants
To implement the a21featureRequest
pattern in the patterns.pl
file, we need a few key ingredients to implement the design choice made and the other design aspects that we discussed:
First, we want the pattern to be available only when either the
a50recipeSelect
or thea50recipeConfirm
is the top level pattern. We should add this as a condition to the rule defining the pattern similar to how we added the agent name condition to the greeting pattern (see Greet, and Self-Identify ).Second, we want to remove any conflicting feature requests. You can use the special action
removeConflicts(Params)
and insert it in the pattern as a move of the agent. This action is defined in thedialog_update.mod2g
file; check it out to better understand what happens. Of course, we need to collect the parametersParams
somehow to tell the agent which feature requests (called parameters here) should be checked for conflicts. Use thegetParamsPatternInitiatingIntent(user, addFilter, Params)
query for this and add it to the condition of each of the rules you implement for thea21featureRequest
pattern variants. The predicate is defined in thedialog.pl
file; check it out to better understand how it works.Third, we need agent intent labels for the two cases that we can end up: we suggest using
ackFilter
for acknowledging there are still recipes that meet all requests, andnoRecipesLeft
when there are no recipes left. The logic that we need to implement to make these moves work differently will be implemented in theresponses.pl
file below.
You should now also have gotten an idea of the overall structure of the sequence of moves that should be implemented for all feature request pattern variations:
The pattern starts with an addFilter intent of the user.
Then the agent should remove conflicting filters using the special action
removeConflicts(Params)
.Then the agent should make either acknowledge with an
ackFilter
move or make anoRecipesLeft
move to indicate that there are no recipes left. Optionally, you could also add afeatureRemovalRequest
move after anoRecipesLeft
move to ask the user to remove one of the features again (we’ll get back to this later).And, finally, if the top level context is
a50recipeConfirm
, then the agent should perform the special actioninsert(a50recipeSelect)
to move back to the recipe selection stage.
In total, you should be implemented four variants of a21featureRequest
pattern for the different conditions: 2 main variants x 2 cases.
Implementing agent responses
We need to create responses in responses.pl
for all the agent intents in the patterns that we added to the patterns.pl
file: ackFilter
, noRecipesLeft
, and featureRemovalRequest
.
For the ackFilter
intent, we provide one complete example that you can add to the responses.pl
file:
The intent ackFilter
acknowledges that the filters the user requested have been applied, and there are still recipes left after doing so. The first query in the body of the rule checks the list of filtered recipes is not empty. The second query collects the filters the user requested in the Params
argument. The third query is predefined in the dialogflow.pl
file; check out this file to understand what the query does. The remaining queries concatenate the various parts of the final textual string response in the output argument Txt
.
You should add one or more variants of the rule to vary the textual responses the agent gives when acknowledging one or more filters have been applied.
For the noRecipesLeft
intent, you need to implement a Prolog rule that generates an appropriate textual response for the scenario where there are no recipes left after filtering:
The rule should have
text(noRecipesLeft, ...)
as head. The second argument is the response message you want your agent to say to the user. You can use a simple string and replace the...
, for example, with the message: "I added your request but I could not find a recipe that matches all of your preferences. Please remove a filter". Please add some variants yourself.The body of the rule should check that there are no recipes left (the condition which makes the response above appropriate). The condition should be that the list of filtered recipes is empty. Check the first query in the rule for
ackFilter
to get an idea on how to do that.
For the featureRemovalRequest
intent, you should add a simple text/2
fact. Use as response text, for example, "Can you have a look again and remove one of your recipe requirements?".
Visuals
We want to differentiate what we show to a user depending on the number of recipes that still meet the user’s requests. The basic idea is that we should not show a large number of recipes to a user but we can show recipe details when the number of remaining recipes becomes sufficiently small (we chose <16, see also above). As a consequence, we want to create two different versions of the a50recipeSelect
page that we created for Request a Recommendation : one that just shows the feature requests made when there are more than 15 recipes that meet these requests, and another for when there are less that 16 which shows the recipe details (titles and pictures) for all of the remaining recipes.
Extend Recipe Overview 1 page
Start by extending the code for the recipe recommendation page that we already created. The first thing we want to do is add a condition for when to show this page. This page should be shown when there is still a long list of recipes that match the requests made thus far by a user. We would like to show this page when the number of filtered recipes is still above 15. Use the recipesFiltered/1
, check the length of the list of recipes this predicate returns, and add a condition that makes sure it consists of more than 15 recipes.
You should also add more information to this first a50recipeSelect
page. The main requirement is that the page shows all the user’s feature requests or filters. You can collect these from the agent’s memory.
Recipe Overview 2 page
A simple way to start writing some code for this page is to copy-paste the code you already have for the recipe recommendation page and modify it. Most importantly, you need to make sure this page is only shown if the number of filtered recipes is below or equal to 15 and if the user is still trying to select a recipe.
Continue with changing the layout. The main requirement is that an overview of the remaining recipe titles and pictures should be shown. Check out W3Schools.com and how we already used a card for the recipe confirmation page for Request a Recommendation .
Your final page should look something like this (just an example, you should be able to easily improve!).
Extend the Recipe Confirmation page
You should extend the initial version of the recipe confirmation page that we created for Request a Recommendation . The main requirement is that the page now also will show the recipe instructions, the ingredient list with quantities, the duration, and number of servings. You may also want to add the feature requests to the recipe confirmation page, but we leave this design choice up to you. You definitely may want to put more work in the styling of the content and the layout of this page to make it look appealing.
Test it Out
Test it out by Run your Conversational Agent. Try different scenarios where you filter on ingredients, ingredient types, and/or cuisine. For example, try adding requests for the following ingredients one by one in order: ginger, salt, sugar, rice. You should end up with exactly 2 recipes left.
Try to filter by Jamaican recipes and see if you receive the following:
Done? Continue with Confirmation and Closing