├── .gitignore
├── img
└── screencast.png
├── .eslintrc.js
├── app
├── dialogs
│ ├── categories.js
│ ├── welcome.js
│ ├── showVariant.js
│ ├── showCart.js
│ ├── choseVariant.js
│ ├── showProduct.js
│ ├── addToCart.js
│ └── explore.js
├── recommendations.js
├── recognizer
│ ├── greeting.js
│ ├── smiles.js
│ └── commands.js
├── sentiment.js
└── search
│ └── search.js
├── index.html
├── indexes
├── README.md
├── categories.json
├── variants.json
├── products.json
└── populate.js
├── recommendations
├── repeater.js
├── populate.js
├── README.md
└── sdk.js
├── LICENSE.md
├── package.json
├── app.js
├── lib
└── promisify-moltin.js
├── README.md
└── luis
└── commercehcat.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
3 | node_modules
4 | localStorage
--------------------------------------------------------------------------------
/img/screencast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pveller/ecommerce-chatbot/HEAD/img/screencast.png
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "prettier",
3 | "parserOptions": {
4 | "ecmaVersion": 8,
5 | "sourceType": "module",
6 | "ecmaFeatures": {
7 | "experimentalObjectRestSpread": true
8 | }
9 | },
10 | "env": {
11 | "es6": true,
12 | "node": true
13 | },
14 | "plugins": ["prettier"],
15 | "rules": {
16 | "prettier/prettier": ["error", { "singleQuote": true }]
17 | }
18 | };
--------------------------------------------------------------------------------
/app/dialogs/categories.js:
--------------------------------------------------------------------------------
1 | const search = require('../search/search');
2 |
3 | module.exports = function(bot) {
4 | bot.dialog('/categories', [
5 | function(session, args, next) {
6 | session.sendTyping();
7 |
8 | search.listTopLevelCategories().then(value => next(value));
9 | },
10 | function(session, args, next) {
11 | const message = (args || [])
12 | .map(v => v.title)
13 | .filter(t => t !== 'Uncategorized')
14 | .join(', ');
15 |
16 | session.endDialog('We have ' + message);
17 | }
18 | ]);
19 | };
20 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | E-Commerce Chatbot Prototype
7 |
8 |
9 |
10 |
11 |
14 |
15 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/dialogs/welcome.js:
--------------------------------------------------------------------------------
1 | module.exports = function(bot) {
2 | bot.dialog('/welcome', [
3 | function(session, args, next) {
4 | const lastVisit = session.userData.lastVisit;
5 |
6 | session.send(['Hello!', 'Hi there!', 'Hi!']);
7 |
8 | if (!lastVisit) {
9 | session.send(
10 | 'Our store carries bikes, parts, accessories, and sport clothing articles'
11 | );
12 | session.userData = Object.assign({}, session.userData, {
13 | lastVisit: new Date()
14 | });
15 | session.save();
16 | } else {
17 | session.send("Glad you're back!");
18 | }
19 |
20 | session.endDialog('How can I help you?');
21 | }
22 | ]);
23 | };
24 |
--------------------------------------------------------------------------------
/app/recommendations.js:
--------------------------------------------------------------------------------
1 | const request = require('request-promise-native');
2 | const _ = require('lodash');
3 |
4 | const url =
5 | 'https://westus.api.cognitive.microsoft.com/recommendations/v4.0/models/';
6 | const apiKey = process.env.RECOMMENDATION_API_KEY;
7 | const model = process.env.RECOMMENDATION_MODEL;
8 | const build = process.env.RECOMMENDATION_BUILD;
9 |
10 | module.exports = {
11 | recommend: function(skus, howmany = 3, threashold = 0.5) {
12 | return request({
13 | url:
14 | `${url}/${model}/recommend/item?build=${build}` +
15 | `&itemIds=${skus.join(',')}` +
16 | `&numberOfResults=${howmany}` +
17 | `&minimalScore=${threashold}` +
18 | `&includeMetadata=false`,
19 | headers: { 'Ocp-Apim-Subscription-Key': `${apiKey}` }
20 | }).then(result => {
21 | const obj = JSON.parse(result) || {};
22 |
23 | return obj.recommendedItems;
24 | });
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/app/recognizer/greeting.js:
--------------------------------------------------------------------------------
1 | const greetings = [
2 | 'hi',
3 | 'hi there',
4 | 'hey',
5 | 'hey there',
6 | 'hello',
7 | 'hello hello',
8 | 'hello there',
9 | 'what up',
10 | "what's up",
11 | 'whatup',
12 | 'salute',
13 | 'morning',
14 | 'good morning',
15 | 'how are you',
16 | 'how r u'
17 | ];
18 |
19 | module.exports = {
20 | recognize: function(context, callback) {
21 | const text = context.message.text
22 | .replace(/[!?,.\/\\\[\]\{\}\(\)]/g, '')
23 | .trim()
24 | .toLowerCase();
25 |
26 | const recognized = {
27 | entities: [],
28 | intent: null,
29 | matched: undefined,
30 | expression: undefined,
31 | intents: [],
32 | score: 0
33 | };
34 |
35 | if (greetings.some(phrase => text === phrase)) {
36 | recognized.intent = 'Greeting';
37 | recognized.score = 1;
38 | }
39 |
40 | callback.call(null, null, recognized);
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/indexes/README.md:
--------------------------------------------------------------------------------
1 | # Azure Search Indexes
2 |
3 | The bot needs three indexes in Azure Search: `categories`, `products`, and `variants`. You can run `populate.js` to get it all set up:
4 |
5 | 1) Sign up for Azure and create a new Azure Search service. I named mine `commercechat`.
6 |
7 | 2) Set up two environment variables just like you need for the bot:
8 |
9 | * `SEARCH_APP_NAME` - the name of your [Azure Search](https://azure.microsoft.com/en-us/services/search) service. The code assumes that you have all three indexes in the same Azure Search resource
10 | * `SEARCH_API_KEY`- your API key to the [Azure Search](https://azure.microsoft.com/en-us/services/search) service
11 |
12 | 3) Run the script like this from the project root:
13 |
14 | ```bash
15 | $ node indexes/populate.js
16 | ```
17 |
18 | Here's how you would run the script with the environment variables set for the time of its execution:
19 |
20 | ```bash
21 | $ SEARCH_APP_NAME=commercechat SEARCH_API_KEY=mykey node indexes/populate.js
22 | ```
23 |
24 |
25 |
--------------------------------------------------------------------------------
/recommendations/repeater.js:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | A simple function that will repeat the operation until a condition is satisfied.
4 | The operation will be repeated asynchronously, with optional delay, and without blowing up the stack.
5 |
6 | */
7 | module.exports = {
8 | repeat: (operation, options) => {
9 | const run = delay =>
10 | new Promise((resolve, reject) => {
11 | setTimeout(
12 | () =>
13 | operation()
14 | .then(response => {
15 | if (options.until(response)) {
16 | if (options.success) {
17 | options.success();
18 | }
19 | resolve();
20 | } else {
21 | if (options.next) {
22 | options.next(response, delay);
23 | }
24 | resolve(run(options.delay));
25 | }
26 | })
27 | .catch(reject),
28 | delay
29 | );
30 | });
31 |
32 | return run(options.delay);
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016-present Pavel Veller
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "engines": {
3 | "node": "8.9.4"
4 | },
5 | "name": "ecommerce-chatbot",
6 | "version": "2.0.0",
7 | "description": "e-commerce chatbot with product recommendations",
8 | "main": "app.js",
9 | "scripts": {
10 | "precommit": "npm run lint",
11 | "lint": "node node_modules/eslint/bin/eslint ./ --fix -f table || true"
12 | },
13 | "dependencies": {
14 | "@moltin/sdk": "^3.3.0",
15 | "botbuilder": "^3.14.0",
16 | "express": "^4.16.2",
17 | "lodash": "^4.17.4",
18 | "request-promise-native": "^1.0.5"
19 | },
20 | "devDependencies": {
21 | "babel-eslint": "^8.2.1",
22 | "eslint": "^4.15.0",
23 | "eslint-config-prettier": "^2.9.0",
24 | "eslint-config-standard": "^11.0.0-beta.0",
25 | "eslint-plugin-import": "^2.8.0",
26 | "eslint-plugin-node": "^5.2.1",
27 | "eslint-plugin-prettier": "^2.4.0",
28 | "eslint-plugin-promise": "^3.6.0",
29 | "eslint-plugin-standard": "^3.0.1",
30 | "husky": "^0.14.3",
31 | "lint-staged": "^6.0.0",
32 | "prettier": "^1.10.2"
33 | },
34 | "keywords": [
35 | "ecommerce",
36 | "chatbot",
37 | "recommendations"
38 | ],
39 | "author": "Pavel Veller",
40 | "license": "MIT"
41 | }
42 |
--------------------------------------------------------------------------------
/app/dialogs/showVariant.js:
--------------------------------------------------------------------------------
1 | const builder = require('botbuilder');
2 | const search = require('../search/search');
3 |
4 | const showVariant = function(session, product, variant) {
5 | session.sendTyping();
6 |
7 | const description =
8 | `${variant.color ? 'Color - ' + variant.color + '\n' : ''}` +
9 | `${variant.size ? 'Size - ' + variant.size : ''}`;
10 |
11 | const tile = new builder.HeroCard(session)
12 | .title(product.title)
13 | .subtitle(`$${variant.price}`)
14 | .text(description || product.description)
15 | .buttons([
16 | builder.CardAction.postBack(session, `@add:${variant.id}`, 'Add To Cart')
17 | ])
18 | .images([builder.CardImage.create(session, product.image)]);
19 |
20 | session.send(
21 | new builder.Message(session)
22 | .text('I am ready when you are')
23 | .attachments([tile])
24 | );
25 | };
26 |
27 | module.exports = function(bot) {
28 | bot.dialog('/showVariant', [
29 | function(session, args, next) {
30 | if (!args || !args.product || !args.variant) {
31 | return session.endDialog(
32 | 'Sorry, I got distracted and lost track of our conversation. Where were we?'
33 | );
34 | }
35 |
36 | showVariant(session, args.product, args.variant);
37 |
38 | session.endDialog();
39 | }
40 | ]);
41 | };
42 |
--------------------------------------------------------------------------------
/indexes/categories.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "categories",
3 | "fields": [
4 | {
5 | "name": "id",
6 | "type": "Edm.String",
7 | "searchable": false,
8 | "filterable": false,
9 | "retrievable": true,
10 | "sortable": false,
11 | "facetable": false,
12 | "key": true,
13 | "analyzer": null
14 | },
15 | {
16 | "name": "title",
17 | "type": "Edm.String",
18 | "searchable": true,
19 | "filterable": false,
20 | "retrievable": true,
21 | "sortable": true,
22 | "facetable": false,
23 | "key": false,
24 | "analyzer": "standard.lucene"
25 | },
26 | {
27 | "name": "description",
28 | "type": "Edm.String",
29 | "searchable": true,
30 | "filterable": false,
31 | "retrievable": true,
32 | "sortable": false,
33 | "facetable": false,
34 | "key": false,
35 | "analyzer": "standard.lucene"
36 | },
37 | {
38 | "name": "parent",
39 | "type": "Edm.String",
40 | "searchable": false,
41 | "filterable": true,
42 | "retrievable": true,
43 | "sortable": false,
44 | "facetable": false,
45 | "key": false,
46 | "analyzer": null
47 | }
48 | ],
49 | "scoringProfiles": [],
50 | "defaultScoringProfile": "",
51 | "corsOptions": {
52 | "allowedOrigins": [
53 | "*"
54 | ],
55 | "maxAgeInSeconds": 300
56 | },
57 | "suggesters": []
58 | }
--------------------------------------------------------------------------------
/app/recognizer/smiles.js:
--------------------------------------------------------------------------------
1 | const detectors = {
2 | emulator: {
3 | pattern: '[:;]-?[()DE]'
4 | },
5 | webchat: {
6 | pattern: '[:;]-?[()DE]'
7 | },
8 | skype: {
9 | pattern: '(.+?)',
10 | smileAt: 1
11 | }
12 | };
13 |
14 | module.exports = {
15 | detect: function(text, channel) {
16 | console.log('Looking for smile in [%s] on [%s]', text, channel);
17 |
18 | const detector = detectors[channel];
19 | if (!detector) {
20 | return undefined;
21 | }
22 |
23 | const smiles = text.match(new RegExp(detector.pattern));
24 | if (!smiles) {
25 | return undefined;
26 | }
27 |
28 | return smiles[detector.smileAt || 0];
29 | },
30 |
31 | recognize: function(context, callback) {
32 | const text = context.message.text;
33 | const channel = context.message.address.channelId;
34 |
35 | const smile = this.detect(text, channel);
36 |
37 | if (!smile) {
38 | callback();
39 | } else {
40 | console.log('Sending back a smily face [%s]', smile);
41 |
42 | callback(null, {
43 | intent: 'Smile',
44 | score: 1,
45 | entities: [
46 | {
47 | entity: smile,
48 | score: 1,
49 | type: 'Smile'
50 | }
51 | ]
52 | });
53 | }
54 | },
55 |
56 | smileBack: function(session) {
57 | const text = session.message.text;
58 | const channel = session.message.address.channelId;
59 |
60 | const smile = this.detect(text, channel);
61 |
62 | if (smile) {
63 | session.send(smile);
64 |
65 | const smiles = new RegExp(detectors[channel].pattern, 'g');
66 | session.message.text = session.message.text.replace(smiles, '');
67 |
68 | if (session.message.text.trim()) {
69 | session.sendTyping();
70 | }
71 | }
72 | }
73 | };
74 |
--------------------------------------------------------------------------------
/app/dialogs/showCart.js:
--------------------------------------------------------------------------------
1 | const builder = require('botbuilder');
2 | const sentiment = require('../sentiment');
3 |
4 | const displayCart = function(session, cart) {
5 | const cards = cart.map(item =>
6 | new builder.ThumbnailCard(session)
7 | .title(item.product.title)
8 | .subtitle(`$${item.variant.price}`)
9 | .text(
10 | `${item.variant.color ? 'Color -' + item.variant.color + '\n' : ''}` +
11 | `${item.variant.size ? 'Size -' + item.variant.size : ''}` ||
12 | item.product.description
13 | )
14 | .buttons([
15 | builder.CardAction.imBack(
16 | session,
17 | `@remove:${item.variant.id}`,
18 | 'Remove'
19 | )
20 | ])
21 | .images([builder.CardImage.create(session, item.variant.image)])
22 | );
23 |
24 | session.sendTyping();
25 | session.send(
26 | new builder.Message(
27 | session,
28 | `You have ${cart.length} products in your cart`
29 | )
30 | .attachments(cards)
31 | .attachmentLayout(builder.AttachmentLayout.carousel)
32 | );
33 | };
34 |
35 | module.exports = function(bot) {
36 | bot.dialog('/showCart', [
37 | function(session, args, next) {
38 | const cart = session.privateConversationData.cart;
39 |
40 | if (!cart || !cart.length) {
41 | session.send(
42 | 'Your shopping cart appears to be empty. Can I help you find anything?'
43 | );
44 | session.reset('/categories');
45 | } else {
46 | displayCart(session, cart);
47 | next();
48 | }
49 | },
50 | ...sentiment.confirm('Ready to checkout?'),
51 | function(session, args, next) {
52 | if (args.response) {
53 | session.reset('/checkout');
54 | } else {
55 | session.endDialog('Sure, take your time. Just tell me when');
56 | }
57 | }
58 | ]);
59 | };
60 |
--------------------------------------------------------------------------------
/indexes/variants.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "variants",
3 | "fields": [
4 | {
5 | "name": "id",
6 | "type": "Edm.String",
7 | "searchable": false,
8 | "filterable": true,
9 | "retrievable": true,
10 | "sortable": false,
11 | "facetable": false,
12 | "key": true,
13 | "analyzer": null
14 | },
15 | {
16 | "name": "productId",
17 | "type": "Edm.String",
18 | "searchable": false,
19 | "filterable": true,
20 | "retrievable": true,
21 | "sortable": false,
22 | "facetable": false,
23 | "key": false,
24 | "analyzer": null
25 | },
26 | {
27 | "name": "color",
28 | "type": "Edm.String",
29 | "searchable": true,
30 | "filterable": true,
31 | "retrievable": true,
32 | "sortable": true,
33 | "facetable": true,
34 | "key": false,
35 | "analyzer": "standard.lucene"
36 | },
37 | {
38 | "name": "size",
39 | "type": "Edm.String",
40 | "searchable": true,
41 | "filterable": true,
42 | "retrievable": true,
43 | "sortable": true,
44 | "facetable": true,
45 | "key": false,
46 | "analyzer": "standard.lucene"
47 | },
48 | {
49 | "name": "sku",
50 | "type": "Edm.String",
51 | "searchable": true,
52 | "filterable": true,
53 | "retrievable": true,
54 | "sortable": false,
55 | "facetable": false,
56 | "key": false,
57 | "analyzer": "standard.lucene"
58 | },
59 | {
60 | "name": "price",
61 | "type": "Edm.Double",
62 | "searchable": false,
63 | "filterable": false,
64 | "retrievable": true,
65 | "sortable": true,
66 | "facetable": true,
67 | "key": false,
68 | "analyzer": null
69 | },
70 | {
71 | "name": "image",
72 | "type": "Edm.String",
73 | "searchable": false,
74 | "filterable": false,
75 | "retrievable": true,
76 | "sortable": false,
77 | "facetable": false,
78 | "key": false,
79 | "analyzer": null
80 | }
81 | ],
82 | "scoringProfiles": [],
83 | "defaultScoringProfile": "",
84 | "corsOptions": null,
85 | "suggesters": []
86 | }
87 |
--------------------------------------------------------------------------------
/app/recognizer/commands.js:
--------------------------------------------------------------------------------
1 | const unrecognized = {
2 | entities: [],
3 | intent: null,
4 | intents: [],
5 | score: 0
6 | };
7 |
8 | const parse = {
9 | parse: function(context, text) {
10 | const parts = text.split(':');
11 | const command = parts[0];
12 |
13 | console.log('Resolved [%s] as [%s] command', text, command);
14 |
15 | const action = this[command] || this[command.slice(1)];
16 | if (!action) {
17 | return unrecognized;
18 | } else {
19 | return action.call(this, context, ...parts.slice(1));
20 | }
21 | },
22 |
23 | list: (context, parent) => ({
24 | entities: [
25 | {
26 | entity: parent,
27 | score: 1,
28 | type: 'Entity'
29 | }
30 | ],
31 | intent: parent ? 'Explore' : 'ShowTopCategories',
32 | score: 1
33 | }),
34 | search: (context, query) => ({
35 | entities: [
36 | {
37 | entity: query,
38 | score: 1,
39 | type: 'Entity'
40 | }
41 | ],
42 | intent: 'Explore',
43 | score: 1
44 | }),
45 | next: () => ({
46 | intent: 'Next',
47 | score: 1
48 | }),
49 | more: function(context, product) {
50 | return this.next(context, product);
51 | },
52 | show: (context, product) => ({
53 | entities: [
54 | {
55 | entity: product,
56 | score: 1,
57 | type: 'Product'
58 | }
59 | ],
60 | intent: 'ShowProduct',
61 | score: 1
62 | }),
63 | cart: () => ({
64 | intent: 'ShowCart',
65 | score: 1
66 | }),
67 | add: (context, what) => ({
68 | entities: [
69 | {
70 | entity: what,
71 | score: 1,
72 | type: 'Id'
73 | }
74 | ],
75 | intent: 'AddToCart',
76 | score: 1
77 | }),
78 | remove: (context, what) => {
79 | console.log('Remove from cart is not supported');
80 | return unrecognized;
81 | },
82 | checkout: () => ({
83 | intent: 'Checkout',
84 | score: 1
85 | }),
86 | reset: () => ({
87 | intent: 'Reset',
88 | score: 1
89 | })
90 | };
91 |
92 | module.exports = {
93 | recognize: function(context, callback) {
94 | const text = context.message.text;
95 |
96 | if (!text.startsWith('@') && !['next', 'more'].includes(text)) {
97 | callback.call(null, null, unrecognized);
98 | } else {
99 | callback.call(null, null, parse.parse(context, text));
100 | }
101 | }
102 | };
103 |
--------------------------------------------------------------------------------
/app/dialogs/choseVariant.js:
--------------------------------------------------------------------------------
1 | const builder = require('botbuilder');
2 |
3 | module.exports = function(bot) {
4 | bot.dialog('/choseVariant', [
5 | function(session, args, next) {
6 | const item = (session.dialogData.product = args.product);
7 |
8 | if (!item.modifiers.includes('color')) {
9 | next();
10 | } else if (item.color.length === 1) {
11 | builder.Prompts.confirm(
12 | session,
13 | `${item.title} only comes in one color - ${
14 | item.color[0]
15 | }. Do you like it?`,
16 | {
17 | listStyle: builder.ListStyle.button
18 | }
19 | );
20 | } else {
21 | builder.Prompts.choice(
22 | session,
23 | `Please select what color you'd like best on your ${item.title}`,
24 | item.color,
25 | {
26 | listStyle: builder.ListStyle.button
27 | }
28 | );
29 | }
30 | },
31 | function(session, args, next) {
32 | if (session.message.text === 'no') {
33 | return session.endDialog(
34 | "Well, sorry. Come check next time. Maybe we'll have it in the color you like. Thanks!"
35 | );
36 | }
37 |
38 | const item = session.dialogData.product;
39 | // ToDo: response comes back as [true] if the user accepted the single color there was
40 | session.dialogData.color = args.response || item.color[0];
41 | session.save();
42 |
43 | if (!item.modifiers.includes('size')) {
44 | next();
45 | } else if (item.size.length === 1) {
46 | builder.Prompts.confirm(
47 | session,
48 | `${item.title} only comes in one size - ${item.size[0]}. Is that ok?`,
49 | {
50 | listStyle: builder.ListStyle.button
51 | }
52 | );
53 | } else {
54 | builder.Prompts.choice(
55 | session,
56 | `Please select a size for your ${item.title}`,
57 | item.size,
58 | {
59 | listStyle: builder.ListStyle.button
60 | }
61 | );
62 | }
63 | },
64 | function(session, args, next) {
65 | if (session.message.text === 'no') {
66 | return session.endDialog(
67 | "Well, sorry. Come check next time. Maybe we'll have your size in stock. Thanks!"
68 | );
69 | }
70 |
71 | const item = session.dialogData.product;
72 |
73 | session.dialogData.size = args.response || item.size[0];
74 | session.save();
75 |
76 | session.endDialogWithResult({
77 | response: {
78 | color: session.dialogData.color,
79 | size: session.dialogData.size
80 | }
81 | });
82 | }
83 | ]);
84 | };
85 |
--------------------------------------------------------------------------------
/recommendations/populate.js:
--------------------------------------------------------------------------------
1 | const request = require('request-promise-native');
2 | const fs = require('fs');
3 | const path = require('path');
4 | const sdk = require('./sdk')(process.env.RECOMMENDATION_API_KEY);
5 | const repeater = require('./repeater');
6 |
7 | const catalog = process.argv[2];
8 | if (!catalog || !fs.existsSync(catalog)) {
9 | throw 'Please specify a valid file system path where you generated recommendations-catalog.csv and recommendations-usage.csv';
10 | }
11 |
12 | const modelName = process.argv[3] || 'eComm-Chatbot';
13 | const description = process.argv[4] || 'Adventure Works Recommendations';
14 |
15 | let model = undefined;
16 | let build = undefined;
17 |
18 | sdk.model
19 | .list()
20 | .then(({ models }) => {
21 | const model = models.find(m => m.name === modelName);
22 | if (model) {
23 | console.log(
24 | `There is already a recommendation model named ${modelName}. The existing model needs to be deleted first`
25 | );
26 |
27 | return sdk.model.delete(model.id);
28 | } else {
29 | return Promise.resolve();
30 | }
31 | })
32 | .then(() => {
33 | return sdk.model.create(modelName, description);
34 | })
35 | .then(result => {
36 | model = result;
37 |
38 | console.log(`Model ${model.id} created succesfully`);
39 | })
40 | .then(() => {
41 | return sdk.upload.catalog(
42 | model.id,
43 | 'AdventureWorks',
44 | path.resolve(catalog, 'recommendations-catalog.csv')
45 | );
46 | })
47 | .then(() => {
48 | return sdk.upload.usage(
49 | model.id,
50 | 'OnlineSales',
51 | path.resolve(catalog, 'recommendations-usage.csv')
52 | );
53 | })
54 | .then(() => {
55 | return sdk.build.fbt(model.id, 'FBT build for Adventure Works');
56 | })
57 | .then(result => {
58 | build = result;
59 |
60 | console.log(
61 | `FBT build ${
62 | build.buildId
63 | } created succesfully. Will now wait for the training to finish.`
64 | );
65 |
66 | return repeater.repeat(() => sdk.build.get(model.id, build.buildId), {
67 | delay: 30000,
68 | until: response => !['NotStarted', 'Running'].includes(response.status),
69 | done: response =>
70 | console.log(`Build training finished: ${response.status}`),
71 | next: (response, delay) =>
72 | console.log(
73 | `Training is ${response.status}. Will check again in ${delay /
74 | 1000} seconds...`
75 | )
76 | });
77 | })
78 | .then(() => {
79 | console.log('All said and done');
80 | console.log(`Set RECOMMENDATION_MODEL to ${model.id}`);
81 | console.log(`Set RECOMMENDATION_BUILD to ${build.buildId}`);
82 | })
83 | .catch(error => {
84 | console.error(error);
85 | });
86 |
--------------------------------------------------------------------------------
/app/sentiment.js:
--------------------------------------------------------------------------------
1 | const request = require('request-promise-native');
2 | const builder = require('botbuilder');
3 |
4 | const apiKey = process.env.SENTIMENT_API_KEY;
5 |
6 | module.exports = {
7 | detect: function(text, language = 'en', threshold = 0.05) {
8 | return request({
9 | method: 'POST',
10 | url:
11 | process.env.SENTIMENT_ENDPOINT ||
12 | 'https://westus.api.cognitive.microsoft.com/text/analytics/v2.0/sentiment',
13 | headers: {
14 | 'Content-Type': 'application/json',
15 | 'Ocp-Apim-Subscription-Key': apiKey
16 | },
17 | body: {
18 | documents: [
19 | {
20 | id: '-',
21 | language,
22 | text
23 | }
24 | ]
25 | },
26 | json: true
27 | }).then(result => {
28 | if (result && result.documents) {
29 | const score = result.documents[0].score;
30 |
31 | console.log(`SENTIMENT: ${score} in ${text}`);
32 |
33 | if (score >= 0.5 + threshold) {
34 | return {
35 | response: true,
36 | score
37 | };
38 | } else if (score <= 0.5 - threshold) {
39 | return {
40 | response: false,
41 | score
42 | };
43 | } else {
44 | return Promise.reject(
45 | `${score} is inconclusive to detect sentiment in ${text}`
46 | );
47 | }
48 | } else {
49 | return Promise.reject(
50 | 'No response from the sentiment detection service'
51 | );
52 | }
53 | });
54 | },
55 |
56 | confirm: function(question, reprompt) {
57 | return [
58 | (session, args, next) => {
59 | builder.Prompts.text(session, question);
60 | },
61 | (session, args, next) => {
62 | const answer = builder.EntityRecognizer.parseBoolean(args.response);
63 | if (typeof answer !== 'undefined') {
64 | next({
65 | response: answer
66 | });
67 | } else {
68 | this.detect(args.response)
69 | .then(result => next(result))
70 | .catch(error => {
71 | console.error(error);
72 | next();
73 | });
74 | }
75 | },
76 | (session, args, next) => {
77 | if (args && typeof args.response !== 'undefined') {
78 | next(args);
79 | } else {
80 | reprompt =
81 | reprompt ||
82 | 'I am sorry, I did not understand what you meant. ' +
83 | "See if you can use the buttons or reply with a simple 'yes' or 'no'. " +
84 | 'Sorry again!';
85 |
86 | session.send(reprompt);
87 |
88 | builder.Prompts.confirm(session, question, {
89 | listStyle: builder.ListStyle.button
90 | });
91 | }
92 | }
93 | ];
94 | }
95 | };
96 |
--------------------------------------------------------------------------------
/recommendations/README.md:
--------------------------------------------------------------------------------
1 | # UPDATE
2 |
3 | Microsoft is discontinuing the Recommendations API preview in February 2018. The alternative is to deploy the [Recommendations Solution](https://gallery.cortanaintelligence.com/Tutorial/Recommendations-Solution) template, but it does not yet support the frequently bought together algorithm that the bot was using. The deployment of a solution template is aslo a more involved process so I decided to drop recommendations for now. The recommendation code is still there and is just not used by the dialog flow at the moment.
4 |
5 | # Azure Recommendations API
6 |
7 | First, head over to the [Adventure Works Moltin](https://github.com/pveller/adventureworks-moltin) and run [the script](https://github.com/pveller/adventureworks-moltin/blob/master/recommendations.js) that will generate the data you need to feed the recommendation engine. I didn't re-create historical transactions as orders in Moltin so the script will use the original source data to build the recommendation model.
8 |
9 | You should now have two files alongside your Adventure Works catalog:
10 |
11 | * `recommendations-catalog.csv` - product catalog with attributes
12 | * `recommendations-usage.csv` - the list of historical purchases
13 |
14 | Subscribe for [Azure Recommendations API](https://azure.microsoft.com/en-us/services/cognitive-services/recommendations/) and set the following environment variable:
15 |
16 | * `RECOMMENDATION_API_KEY` - your API key to the Recommendations API
17 |
18 | You can now run:
19 |
20 | ```bash
21 | $ node recommendations/populate.js "/Users/you/path-to-CSVs" "Model_Name" "Model Description"
22 | ```
23 |
24 | or with the environment variable:
25 |
26 | ```bash
27 | $ RECOMMENDATION_API_KEY=yourkey node recommendations/populate.js "/Users/you/path-to-CSVs" "Model_Name" "Model Description"
28 | ```
29 |
30 | If you don't provide the name and/or the description, the script will use the defaults: `eComm-Chatbot` and `FBT build for Adventure Works`.
31 |
32 | The script will output the recommendation **model** and recommendation **build** that was trained. Example output:
33 |
34 | ```
35 | There is already a recommendation model named eComm-Chatbot. The existing model needs to be deleted first
36 | Model 8b33ed58-5d8a-4b5a-a1fb-3a3fcd7e043c created succesfully
37 | Succesfully imported 295 catalog entries
38 | Succesfully imported 121317 sales records
39 | FBT build 1647571 created succesfully. Will now wait for the training to finish.
40 | Training is Running. Will check again in 30 seconds...
41 | Training is Running. Will check again in 30 seconds...
42 | Training is Running. Will check again in 30 seconds...
43 | Training is Running. Will check again in 30 seconds...
44 | Training is Running. Will check again in 30 seconds...
45 | Build training finished: Succeeded
46 | All said and done
47 | Set RECOMMENDATION_MODEL to 8b33ed58-5d8a-4b5a-a1fb-3a3fcd7e043c
48 | Set RECOMMENDATION_BUILD to 1647571
49 | ```
50 |
51 | You will use these values to populate the following environment variables that your ecommerce bot needs to use recommendations:
52 |
53 | * `RECOMMENDATION_MODEL`- you can create multiple recommendation models and this way you can choose which one the bot will use for suggestions
54 | * `RECOMMENDATION_BUILD` - a given model (your product catalog, historical transactions, and business rules) can have multiple recommendation builds and this is how you tell which one the bot will use
55 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const builder = require('botbuilder');
2 | const express = require('express');
3 | const greeting = require('./app/recognizer/greeting');
4 | const commands = require('./app/recognizer/commands');
5 | const smiles = require('./app/recognizer/smiles');
6 |
7 | const dialog = {
8 | welcome: require('./app/dialogs/welcome'),
9 | categories: require('./app/dialogs/categories'),
10 | explore: require('./app/dialogs/explore'),
11 | showProduct: require('./app/dialogs/showProduct'),
12 | choseVariant: require('./app/dialogs/choseVariant'),
13 | showVariant: require('./app/dialogs/showVariant'),
14 | addToCart: require('./app/dialogs/addToCart'),
15 | showCart: require('./app/dialogs/showCart')
16 | };
17 |
18 | const connector = new builder.ChatConnector({
19 | appId: process.env.MICROSOFT_APP_ID,
20 | appPassword: process.env.MICROSFT_APP_PASSWORD
21 | });
22 |
23 | const bot = new builder.UniversalBot(connector, {
24 | persistConversationData: true
25 | });
26 |
27 | var intents = new builder.IntentDialog({
28 | recognizers: [
29 | commands,
30 | greeting,
31 | new builder.LuisRecognizer(process.env.LUIS_ENDPOINT)
32 | ],
33 | intentThreshold: 0.2,
34 | recognizeOrder: builder.RecognizeOrder.series
35 | });
36 |
37 | intents.matches('Greeting', '/welcome');
38 | intents.matches('ShowTopCategories', '/categories');
39 | intents.matches('Explore', '/explore');
40 | intents.matches('Next', '/next');
41 | intents.matches('ShowProduct', '/showProduct');
42 | intents.matches('AddToCart', '/addToCart');
43 | intents.matches('ShowCart', '/showCart');
44 | intents.matches('Checkout', '/checkout');
45 | intents.matches('Reset', '/reset');
46 | intents.matches('Smile', '/smileBack');
47 | intents.onDefault('/confused');
48 |
49 | bot.dialog('/', intents);
50 | dialog.welcome(bot);
51 | dialog.categories(bot);
52 | dialog.explore(bot);
53 | dialog.showProduct(bot);
54 | dialog.choseVariant(bot);
55 | dialog.showVariant(bot);
56 | dialog.addToCart(bot);
57 | dialog.showCart(bot);
58 |
59 | bot.dialog('/confused', [
60 | function(session, args, next) {
61 | // ToDo: need to offer an option to say "help"
62 | if (session.message.text.trim()) {
63 | session.endDialog(
64 | "Sorry, I didn't understand you or maybe just lost track of our conversation"
65 | );
66 | } else {
67 | session.endDialog();
68 | }
69 | }
70 | ]);
71 |
72 | bot.on('routing', smiles.smileBack.bind(smiles));
73 |
74 | bot.dialog('/reset', [
75 | function(session, args, next) {
76 | session.endConversation(['See you later!', 'bye!']);
77 | }
78 | ]);
79 |
80 | bot.dialog('/checkout', [
81 | function(session, args, next) {
82 | const cart = session.privateConversationData.cart;
83 |
84 | if (!cart || !cart.length) {
85 | session.send(
86 | 'I would be happy to check you out but your cart appears to be empty. Look around and see if you like anything'
87 | );
88 | session.reset('/categories');
89 | } else {
90 | session.endDialog('Alright! You are all set!');
91 | }
92 | }
93 | ]);
94 |
95 | const app = express();
96 |
97 | app.get(`/`, (_, res) => res.sendFile(path.join(__dirname + '/index.html')));
98 | app.post('/api/messages', connector.listen());
99 |
100 | app.listen(process.env.PORT || process.env.port || 3978, () => {
101 | console.log('Express HTTP is ready and is accepting connections');
102 | });
103 |
--------------------------------------------------------------------------------
/indexes/products.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "products",
3 | "fields": [
4 | {
5 | "name": "id",
6 | "type": "Edm.String",
7 | "searchable": false,
8 | "filterable": true,
9 | "retrievable": true,
10 | "sortable": false,
11 | "facetable": false,
12 | "key": true,
13 | "analyzer": null
14 | },
15 | {
16 | "name": "title",
17 | "type": "Edm.String",
18 | "searchable": true,
19 | "filterable": true,
20 | "retrievable": true,
21 | "sortable": false,
22 | "facetable": false,
23 | "key": false,
24 | "analyzer": "standard.lucene"
25 | },
26 | {
27 | "name": "description",
28 | "type": "Edm.String",
29 | "searchable": true,
30 | "filterable": false,
31 | "retrievable": true,
32 | "sortable": false,
33 | "facetable": false,
34 | "key": false,
35 | "analyzer": "standard.lucene"
36 | },
37 | {
38 | "name": "category",
39 | "type": "Edm.String",
40 | "searchable": true,
41 | "filterable": true,
42 | "retrievable": true,
43 | "sortable": true,
44 | "facetable": true,
45 | "key": false,
46 | "analyzer": "standard.lucene"
47 | },
48 | {
49 | "name": "categoryId",
50 | "type": "Edm.String",
51 | "searchable": false,
52 | "filterable": true,
53 | "retrievable": true,
54 | "sortable": false,
55 | "facetable": false,
56 | "key": false,
57 | "analyzer": null
58 | },
59 | {
60 | "name": "subcategory",
61 | "type": "Edm.String",
62 | "searchable": true,
63 | "filterable": true,
64 | "retrievable": true,
65 | "sortable": true,
66 | "facetable": true,
67 | "key": false,
68 | "analyzer": "standard.lucene"
69 | },
70 | {
71 | "name": "subcategoryId",
72 | "type": "Edm.String",
73 | "searchable": false,
74 | "filterable": true,
75 | "retrievable": true,
76 | "sortable": false,
77 | "facetable": false,
78 | "key": false,
79 | "analyzer": null
80 | },
81 | {
82 | "name": "modifiers",
83 | "type": "Collection(Edm.String)",
84 | "searchable": false,
85 | "filterable": false,
86 | "retrievable": true,
87 | "sortable": false,
88 | "facetable": false,
89 | "key": false,
90 | "analyzer": null
91 | },
92 | {
93 | "name": "color",
94 | "type": "Collection(Edm.String)",
95 | "searchable": true,
96 | "filterable": true,
97 | "retrievable": true,
98 | "sortable": false,
99 | "facetable": true,
100 | "key": false,
101 | "analyzer": "standard.lucene"
102 | },
103 | {
104 | "name": "size",
105 | "type": "Collection(Edm.String)",
106 | "searchable": true,
107 | "filterable": true,
108 | "retrievable": true,
109 | "sortable": false,
110 | "facetable": true,
111 | "key": false,
112 | "analyzer": "standard.lucene"
113 | },
114 | {
115 | "name": "price",
116 | "type": "Edm.Double",
117 | "searchable": false,
118 | "filterable": false,
119 | "retrievable": true,
120 | "sortable": false,
121 | "facetable": true,
122 | "key": false,
123 | "analyzer": null
124 | },
125 | {
126 | "name": "image",
127 | "type": "Edm.String",
128 | "searchable": false,
129 | "filterable": false,
130 | "retrievable": true,
131 | "sortable": false,
132 | "facetable": false,
133 | "key": false,
134 | "analyzer": null
135 | }
136 | ],
137 | "scoringProfiles": [],
138 | "defaultScoringProfile": "",
139 | "corsOptions": null,
140 | "suggesters": []
141 | }
142 |
--------------------------------------------------------------------------------
/lib/promisify-moltin.js:
--------------------------------------------------------------------------------
1 | const request = require('request');
2 | const fs = require('fs');
3 |
4 | const promisify = moltin => {
5 | const promisified = {};
6 |
7 | const executor = (actor, action) =>
8 | function() {
9 | const args = [...arguments];
10 |
11 | let success = (result, pagination) => {
12 | if (result) {
13 | result.pagination = pagination;
14 | }
15 |
16 | return result;
17 | };
18 |
19 | let error = (error, details) => details;
20 |
21 | if (typeof args[args.length - 1] === 'function') {
22 | if (typeof args[args.length - 2] === 'function') {
23 | error = args.pop();
24 | success = args.pop();
25 | } else {
26 | success = args.pop();
27 | }
28 | }
29 |
30 | return new Promise((resolve, reject) => {
31 | moltin.Authenticate(function() {
32 | actor[action].call(
33 | actor,
34 | ...args,
35 | (result, pagination) => {
36 | resolve(success.call(null, result, pagination));
37 | },
38 | (err, details) => {
39 | console.error(details);
40 | reject(error.call(null, details));
41 | }
42 | );
43 | });
44 | });
45 | };
46 |
47 | Object.keys(moltin)
48 | .filter(key => key !== 'options' && typeof moltin[key] === 'object')
49 | .forEach(member => {
50 | promisified[member] = {};
51 | let actor = moltin[member];
52 |
53 | Object.keys(actor.__proto__)
54 | .concat(Object.keys(actor.__proto__.__proto__))
55 | .filter(action => typeof actor[action] === 'function')
56 | .forEach(action => {
57 | promisified[member][action] = executor(actor, action);
58 | });
59 | });
60 |
61 | return promisified;
62 | };
63 |
64 | module.exports = function(moltin) {
65 | const promisified = promisify(moltin);
66 |
67 | promisified.Image = {
68 | Create: image =>
69 | new Promise((resolve, reject) => {
70 | moltin.Authenticate(function() {
71 | request.post(
72 | {
73 | url: 'https://api.molt.in/v1/files',
74 | headers: {
75 | Authorization: moltin.options.auth.token,
76 | 'Content-Type': 'multipart/form-data'
77 | },
78 | formData: {
79 | file: fs.createReadStream(image.file),
80 | assign_to: image.assignTo
81 | }
82 | },
83 | (err, response, body) => {
84 | if (response.statusCode !== 200 && response.statusCode !== 201) {
85 | console.error(
86 | 'Failed to upload an image [%s] -> %s',
87 | image.file,
88 | response.body
89 | );
90 | reject(response.body);
91 | }
92 |
93 | resolve(JSON.parse(body));
94 | }
95 | );
96 | });
97 | })
98 | };
99 |
100 | promisified.Product.RemoveAll = function() {
101 | const clean = () => {
102 | return this.List(null).then(products => {
103 | const total = products.pagination.total;
104 | const current = products.pagination.current;
105 |
106 | console.log('Processing the first %s of %s total', current, total);
107 |
108 | const deleted = Promise.all(
109 | products.map(p => {
110 | console.log('Requesting a delete of %s', p.title);
111 | return this.Delete(p.id).catch(() => null);
112 | })
113 | );
114 |
115 | return total <= current ? deleted : deleted.then(() => clean());
116 | });
117 | };
118 |
119 | return clean();
120 | };
121 |
122 | return promisified;
123 | };
124 |
--------------------------------------------------------------------------------
/app/dialogs/showProduct.js:
--------------------------------------------------------------------------------
1 | const builder = require('botbuilder');
2 | const search = require('../search/search');
3 |
4 | const showProduct = function(session, product) {
5 | session.sendTyping();
6 |
7 | const tile = new builder.HeroCard(session)
8 | .title(product.title)
9 | .subtitle(`$${product.price}`)
10 | .text(product.description)
11 | .buttons(
12 | product.modifiers.length === 0 ||
13 | (product.size.length <= 1 && product.color.length <= 1)
14 | ? [
15 | builder.CardAction.postBack(
16 | session,
17 | `@add:${product.id}`,
18 | 'Add To Cart'
19 | )
20 | ]
21 | : []
22 | )
23 | .images([builder.CardImage.create(session, product.image)]);
24 |
25 | session.send(new builder.Message(session).attachments([tile]));
26 | };
27 |
28 | module.exports = function(bot) {
29 | bot.dialog('/showProduct', [
30 | function(session, args, next) {
31 | if (!args) {
32 | return session.reset('/confused');
33 | }
34 |
35 | const product = builder.EntityRecognizer.findEntity(
36 | args.entities,
37 | 'Product'
38 | );
39 |
40 | if (!product || !product.entity) {
41 | builder.Prompts.text(
42 | session,
43 | 'I am sorry, what product would you like to see?'
44 | );
45 | } else {
46 | next({ response: product.entity });
47 | }
48 | },
49 | function(session, args, next) {
50 | session.sendTyping();
51 |
52 | const product = args.response;
53 |
54 | Promise.all([
55 | search.findProductById(product),
56 | search.findProductsByTitle(product)
57 | ])
58 | .then(([product, products]) => {
59 | const item = product.concat(products)[0];
60 | if (!item) {
61 | session.endDialog(
62 | "Sorry, I couldn't find the product you asked about"
63 | );
64 | return Promise.reject();
65 | } else {
66 | return item;
67 | }
68 | })
69 | .then(item => {
70 | showProduct(session, item);
71 | return item;
72 | })
73 | .then(item => {
74 | session.dialogData.product = item;
75 |
76 | if (
77 | item.modifiers.length === 0 ||
78 | (item.size.length <= 1 && item.color.length <= 1)
79 | ) {
80 | next();
81 | } else {
82 | builder.Prompts.confirm(
83 | session,
84 | `This product comes in differnet ` +
85 | item.modifiers.map(mod => `${mod}s`).join(' and ') +
86 | '. Would you like to choose one that fits you?',
87 | { listStyle: builder.ListStyle.button }
88 | );
89 | }
90 | })
91 | .catch(err => {
92 | console.error(err);
93 | });
94 | },
95 | function(session, args, next) {
96 | if (args.response) {
97 | session.beginDialog('/choseVariant', {
98 | product: session.dialogData.product
99 | });
100 | } else if (session.message.text === 'no') {
101 | session.endDialog('Alright. I am here if you need anything else');
102 | } else {
103 | // no variants, can go straight to "add to card"
104 | next();
105 | }
106 | },
107 | function(session, args, next) {
108 | const color =
109 | args &&
110 | args.response &&
111 | args.response.color &&
112 | args.response.color.entity;
113 | const size =
114 | args &&
115 | args.response &&
116 | args.response.size &&
117 | args.response.size.entity;
118 |
119 | // ToDo: I wonder if it's still here after we ran another dialog on top of the current one or if I need to cary it back
120 | const product = session.dialogData.product;
121 |
122 | search.findVariantForProduct(product.id, color, size).then(variant => {
123 | if (color || size) {
124 | session.sendTyping();
125 | session.reset('/showVariant', { product, variant });
126 | } else {
127 | session.endDialog();
128 | }
129 | });
130 | }
131 | ]);
132 | };
133 |
--------------------------------------------------------------------------------
/app/search/search.js:
--------------------------------------------------------------------------------
1 | const request = require('request-promise-native');
2 | const _ = require('lodash');
3 |
4 | const searchApp = process.env.SEARCH_APP_NAME;
5 | const apiKey = process.env.SEARCH_API_KEY;
6 |
7 | const indexes = {
8 | categories: `https://${searchApp}.search.windows.net/indexes/categories/docs?api-version=2015-02-28`,
9 | products: `https://${searchApp}.search.windows.net/indexes/products/docs?api-version=2015-02-28`,
10 | variants: `https://${searchApp}.search.windows.net/indexes/variants/docs?api-version=2015-02-28`
11 | };
12 |
13 | const search = (index, query) => {
14 | return request({
15 | url: `${indexes[index]}&${query}`,
16 | headers: { 'api-key': `${apiKey}` }
17 | })
18 | .then(result => {
19 | const obj = JSON.parse(result);
20 | console.log(
21 | `Searched ${index} for [${query}] and found ${obj &&
22 | obj.value &&
23 | obj.value.length} results`
24 | );
25 | return obj.value;
26 | })
27 | .catch(error => {
28 | console.error(error);
29 | return [];
30 | });
31 | };
32 |
33 | const searchCategories = query => search('categories', query);
34 | const searchProducts = query => search('products', query);
35 | const searchVariants = query => search('variants', query);
36 |
37 | module.exports = {
38 | listTopLevelCategories: () => searchCategories('$filter=parent eq null'),
39 |
40 | findCategoryByTitle: title => searchCategories(`search=title:"${title}~"`),
41 |
42 | findSubcategoriesByParentId: id =>
43 | searchCategories(`$filter=parent eq '${id}'`),
44 |
45 | findSubcategoriesByParentTitle: function(title) {
46 | // ToDo: would be easier if categories had their parent titles indexed
47 | return this.findCategoryByTitle(title).then(value => {
48 | // ToDo: do we care about the test score on the result?
49 | return value.slice(0, 1).reduce((chain, v) => {
50 | return chain.then(() => {
51 | return this.findSubcategoriesByParentId(v.id);
52 | });
53 | }, Promise.resolve({ value: [] }));
54 | });
55 | },
56 |
57 | findProductById: function(product) {
58 | return searchProducts(`$filter=id eq '${product}'`);
59 | },
60 |
61 | findProductsByTitle: function(product) {
62 | return searchProducts(`search=title:'${product}'`);
63 | },
64 |
65 | findProductsBySubcategoryTitle: function(title) {
66 | return searchProducts(`search=subcategory:'${title}'`);
67 | },
68 |
69 | findProducts: function(query) {
70 | return searchProducts(query);
71 | },
72 |
73 | findVariantById: function(id) {
74 | return searchVariants(`$filter=id eq '${id}'`);
75 | },
76 |
77 | findVariantBySku: function(sku) {
78 | return searchVariants(`$filter=sku eq '${sku}'`);
79 | },
80 |
81 | findVariantForProduct: function(productId, color, size) {
82 | return searchVariants(`$filter=productId eq '${productId}'`).then(
83 | variants => {
84 | if (variants.length === 1) {
85 | console.log(`Returning the only variant for ${productId}`);
86 | return variants[0];
87 | } else {
88 | return variants.find(v => {
89 | const isColorMatch = v.color === color || (!v.color && !color);
90 | const isSizeMatch = v.size === size || (!v.size && !size);
91 |
92 | console.log(
93 | `Checking if ${v.id} with ${v.size}-${
94 | v.color
95 | } is the right one for ${size}-${color}`
96 | );
97 |
98 | return (
99 | (!color && !size) ||
100 | (color && !size && isColorMatch) ||
101 | (!color && size && isSizeMatch) ||
102 | (isColorMatch && isSizeMatch)
103 | );
104 | });
105 | }
106 | }
107 | );
108 | },
109 |
110 | find: function(query) {
111 | // search for products and categories and then decide what it is based on best match
112 |
113 | // ToDo: also need to search for a category and then products in it
114 | // if its @search.score is higher than a full text product search
115 |
116 | return Promise.all([
117 | this.findSubcategoriesByParentTitle(query),
118 | this.findProducts(`search=${query}`)
119 | ]).then(([subcategories, products]) => ({ subcategories, products }));
120 | }
121 | };
122 |
--------------------------------------------------------------------------------
/recommendations/sdk.js:
--------------------------------------------------------------------------------
1 | // Wrappers over recommendations REST APIs
2 |
3 | const request = require('request-promise-native');
4 | const fs = require('fs');
5 |
6 | const readFileAndPost = (file, options) =>
7 | new Promise((resolve, reject) => {
8 | fs.readFile(file, (err, data) => {
9 | if (err) {
10 | reject(err);
11 | } else {
12 | request(
13 | Object.assign({}, options, {
14 | body: data
15 | })
16 | )
17 | .then(response => {
18 | resolve(JSON.parse(response));
19 | })
20 | .catch(reject);
21 | }
22 | });
23 | });
24 |
25 | module.exports = apikey => {
26 | const headers = {
27 | 'Ocp-Apim-Subscription-Key': apikey
28 | };
29 |
30 | const headersJson = Object.assign({}, headers, {
31 | 'Content-Type': 'application/json'
32 | });
33 |
34 | const headersBinary = Object.assign({}, headers, {
35 | 'Content-Type': 'application/octet-stream'
36 | });
37 |
38 | return {
39 | model: {
40 | list: () =>
41 | request({
42 | uri:
43 | 'https://westus.api.cognitive.microsoft.com/recommendations/v4.0/models',
44 | headers,
45 | json: true
46 | }),
47 | delete: id =>
48 | request({
49 | uri: `https://westus.api.cognitive.microsoft.com/recommendations/v4.0/models/${id}`,
50 | method: 'DELETE',
51 | headers
52 | }),
53 | create: (modelName, description) =>
54 | request({
55 | uri:
56 | 'https://westus.api.cognitive.microsoft.com/recommendations/v4.0/models',
57 | method: 'POST',
58 | headers: headersJson,
59 | json: true,
60 | body: { modelName, description }
61 | })
62 | },
63 |
64 | upload: {
65 | catalog: (modelId, name, file) =>
66 | readFileAndPost(file, {
67 | uri: `https://westus.api.cognitive.microsoft.com/recommendations/v4.0/models/${modelId}/catalog?catalogDisplayName=${name}`,
68 | method: 'POST',
69 | headers: headersBinary
70 | }).then(results => {
71 | if (
72 | results.errorLineCount > 0 ||
73 | (results.errorSummary && results.errorSummary.length > 0)
74 | ) {
75 | console.warn(
76 | `Catalog imported with ${results.errorLineCount} errors.\n${(
77 | results.errorSummary || []
78 | ).join('\n')} `
79 | );
80 | } else {
81 | console.info(
82 | `Succesfully imported ${
83 | results.importedLineCount
84 | } catalog entries`
85 | );
86 | }
87 | }),
88 |
89 | usage: (modelId, name, file) =>
90 | readFileAndPost(file, {
91 | uri: `https://westus.api.cognitive.microsoft.com/recommendations/v4.0/models/${modelId}/usage?usageDisplayName=${name}`,
92 | method: 'POST',
93 | headers: headersBinary
94 | }).then(results => {
95 | if (
96 | results.errorLineCount > 0 ||
97 | (results.errorSummary && results.errorSummary.length > 0)
98 | ) {
99 | console.warn(
100 | `Usage data imported with ${results.errorLineCount} errors.\n${(
101 | results.errorSummary || []
102 | ).join('\n')} `
103 | );
104 | } else {
105 | console.info(
106 | `Succesfully imported ${results.importedLineCount} sales records`
107 | );
108 | }
109 | })
110 | },
111 |
112 | build: {
113 | fbt: (modelId, description) =>
114 | request({
115 | uri: `https://westus.api.cognitive.microsoft.com/recommendations/v4.0/models/${modelId}/builds`,
116 | method: 'POST',
117 | headers: headersJson,
118 | json: true,
119 | body: {
120 | description,
121 | buildType: 'fbt',
122 | buildParameters: {
123 | // using the defaults
124 | }
125 | }
126 | }),
127 |
128 | get: (modelId, buildId) =>
129 | request({
130 | uri: `https://westus.api.cognitive.microsoft.com/recommendations/v4.0/models/${modelId}/builds/${buildId}`,
131 | headers,
132 | json: true
133 | })
134 | }
135 | };
136 | };
137 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UPDATE 2/2/2018
2 |
3 | * Updated to use Moltin v2 API to generate search indexes. Please make sure you re-create your Moltin store using the most recent version of [the import scripts](https://github.com/pveller/adventureworks-moltin) as there are breaking changes in the data layout.
4 | * LUIS model was re-exported and is now in the 2.1.0 schema version.
5 | * Moved from restify to express
6 | * Also, Microsoft is discontinuing the Recommendations API preview in February 2018. The alternative is to deploy the [Recommendations Solution](https://gallery.cortanaintelligence.com/Tutorial/Recommendations-Solution) template, but it does not yet support the frequently bought together algorithm that the bot was using. The deployment of a solution template is aslo a more involved process so I decided to drop recommendations for now. The recommendation code is still there and is just not used by the dialog flow at the moment.
7 |
8 | Note: the bot is still using the original State API that was deprecated. It works, but the recommendation from Microsoft is to implement a [custom state data](https://docs.microsoft.com/en-us/bot-framework/nodejs/bot-builder-nodejs-state-azure-cosmosdb).
9 |
10 | # E-Commerce Chatbot
11 |
12 | An example of a chatbot built with [Microsoft Bot Framework](https://dev.botframework.com/) and featuring e-commerce capabilities via:
13 |
14 | * [Moltin](https://moltin.com)
15 | * [Azure Search](https://azure.microsoft.com/en-us/services/search)
16 | * ~[Recommendations API](https://www.microsoft.com/cognitive-services/en-us/recommendations-api)~
17 | * [LUIS](https://www.microsoft.com/cognitive-services/en-us/language-understanding-intelligent-service-luis)
18 | * [Text Analytics](https://www.microsoft.com/cognitive-services/en-us/text-analytics-api)
19 |
20 | I presented this bot on [API Strat](http://boston2016.apistrat.com/) in Boston as an example of a [smart app built with cognitive APIs](http://boston2016.apistrat.com/speakers/pavel-veller). This bot is also going to [SATURN](https://saturn2017.sched.com/event/9k2m) and [SYNTAX](https://2017.syntaxcon.com/session/building-smarter-apps-with-cognitive-apis/).
21 |
22 | ## Video
23 |
24 | [](https://www.youtube.com/watch?v=uDar3aLdM_M)
25 |
26 | ## How To Run
27 |
28 | If you would like to run it, you would need:
29 |
30 | * A [Moltin](https://moltin.com) subscription with the [Adventure Works](https://msftdbprodsamples.codeplex.com/releases/view/125550) data (I previously [shared scripts](https://github.com/pveller/adventureworks-moltin) to import Adventure Works data into Moltin)
31 | * [Azure Search](https://azure.microsoft.com/en-us/services/search) service with three indexes - `categories`, `products`, and `variants`. You can find the index definitions and the script that can set up everything you need [here](/indexes)
32 | * ~[Recommendations API](https://www.microsoft.com/cognitive-services/en-us/recommendations-api) endpoint with the FBT (frequently bought together) model trained on historical orders. Here's the [instruction on how to set it all up](/recommendations)~
33 | * Trained [LUIS](https://www.microsoft.com/cognitive-services/en-us/language-understanding-intelligent-service-luis) model for the intents that require NLU to be recognized. You can import [the app that I trained](/luis) to get a head start
34 |
35 | Deploy your bot (I used [Azure App Service](https://azure.microsoft.com/en-us/services/app-service/)) and register it with the [dev.botframework.com](https://dev.botframework.com/).
36 |
37 | Set the following environment variables:
38 |
39 | * `MICROSOFT_APP_ID` - you will get it from the [dev.botframework.com](https://dev.botframework.com/) during registration
40 | * `MICROSFT_APP_PASSWORD` - you will get it from the [dev.botframework.com](https://dev.botframework.com/) during registration
41 | * ~`RECOMMENDATION_API_KEY` - your API key to the [Recommendations API](https://www.microsoft.com/cognitive-services/en-us/recommendations-api) service from the [Microsoft Cognitive Services](https://www.microsoft.com/cognitive-services/)~
42 | * ~`RECOMMENDATION_MODEL`- you can create multiple recommendation models and this way you can choose which one the bot will use for suggestions~
43 | * ~`RECOMMENDATION_BUILD` - a given model (your product catalog, historical transactions, and business rules) can have multiple recommendation builds and this is how you tell which one the bot will use~
44 | * `SEARCH_APP_NAME` - the name of your [Azure Search](https://azure.microsoft.com/en-us/services/search) service. The code assumes that you have all three indexes in the same Azure Search resource
45 | * `SEARCH_API_KEY`- your API key to the [Azure Search](https://azure.microsoft.com/en-us/services/search) service
46 | * `LUIS_ENDPOINT` - the URL of your published LUIS model. Please keep the `Add verbose flag` on and remove `&q=` from the URL. THe bot framework will add it.
47 | * `SENTIMENT_API_KEY` - your API key to the [Text Analytics](https://www.microsoft.com/cognitive-services/en-us/text-analytics-api) service.
48 | * `SENTIMENT_ENDPOINT` - the enpoint of yout [Text Analytics](https://www.microsoft.com/cognitive-services/en-us/text-analytics-api) service. Defaults to `https://westus.api.cognitive.microsoft.com/text/analytics/v2.0/sentiment`
49 |
50 | If you would like to connect the [Bing Spell Check](https://www.microsoft.com/cognitive-services/en-us/bing-spell-check-api) service, you would do so in LUIS when publishing your endpoint. This integration is transparent to the app and all you do is provision your Azure subscription key to the service and connect it to your LUIS app.
51 |
52 | ## To-Do
53 |
54 | * The shopping cart is currently kept in the bot's memory (`session.privateConversationData.cart`) and does not sync back to Moltin
55 | * Checkout process is not integrated with Moltin
56 | * The bot is not multi-lingual
57 |
58 | ## License
59 |
60 | MIT
61 |
--------------------------------------------------------------------------------
/app/dialogs/addToCart.js:
--------------------------------------------------------------------------------
1 | const builder = require('botbuilder');
2 | const search = require('../search/search');
3 | //const recommendations = require('../recommendations');
4 | const sentiment = require('../sentiment');
5 |
6 | const lookupProductOrVariant = function(session, id, next) {
7 | session.sendTyping();
8 |
9 | return Promise.all([
10 | search.findProductById(id),
11 | search.findVariantById(id)
12 | ]).then(([products, variants]) => {
13 | if (products.length) {
14 | product = products[0];
15 | if (
16 | product.modifiers.length === 0 ||
17 | (product.size.length <= 1 && product.color.length <= 1)
18 | ) {
19 | session.sendTyping();
20 |
21 | return search
22 | .findVariantForProduct(product.id)
23 | .then(variant => ({ product, variant }));
24 | } else {
25 | // This would only happen if someone clicked Add To Cart on a multi-variant product
26 | // And I don't think we give the user that option
27 | session.reset('/showProduct', {
28 | entities: [
29 | {
30 | entity: id,
31 | score: 1,
32 | type: 'Product'
33 | }
34 | ]
35 | });
36 | return Promise.reject();
37 | }
38 | } else if (variants.length) {
39 | const variant = variants[0];
40 |
41 | return search
42 | .findProductById(variant.productId)
43 | .then(products => ({
44 | product: products[0],
45 | variant
46 | }))
47 | .catch(error => {
48 | console.error(error);
49 | });
50 | } else {
51 | session.endDialog(`I cannot find ${id} in my product catalog, sorry!`);
52 | return Promise.reject();
53 | }
54 | });
55 | };
56 |
57 | const describe = function(product, variant) {
58 | return (
59 | `${product.title} (${variant.sku})` +
60 | (!!variant.color ? `, Color - ${variant.color}` : '') +
61 | (!!variant.size ? `, Size - ${variant.size}` : '')
62 | );
63 | };
64 |
65 | /*
66 | Not used at the moment.
67 | Microsoft decided to discontinue the Recommendations API preview and the alternative is not exactly the same.
68 | I may come back later and integrate another service
69 | */
70 | /*
71 | const showRecommendations = function(session) {
72 | session.sendTyping();
73 |
74 | Promise.all(
75 | session.dialogData.recommendations.map(offer => {
76 | return new Promise((resolve, reject) => {
77 | search
78 | .findVariantBySku(offer.items[0].id)
79 | .then(variants => {
80 | offer.variant = variants[0];
81 | return offer.variant.productId;
82 | })
83 | .then(productId => search.findProductById(productId))
84 | .then(products => {
85 | offer.product = products[0];
86 | resolve(offer);
87 | })
88 | .catch(error => {
89 | console.error(error);
90 | reject(error);
91 | });
92 | });
93 | })
94 | ).then(offers => {
95 | session.sendTyping();
96 |
97 | // skype doesn't understand postBack from the carousel so that's why I am using imBack for recommendations
98 | const tiles = offers.map(offer =>
99 | new builder.ThumbnailCard(session)
100 | .title(offer.product.title)
101 | .subtitle(`$${offer.product.price}`)
102 | .text(offer.reasoning)
103 | .buttons([
104 | builder.CardAction.imBack(
105 | session,
106 | `@show:${offer.product.id}`,
107 | 'Show me'
108 | )
109 | ])
110 | .images([builder.CardImage.create(session, offer.variant.image)])
111 | );
112 |
113 | session.endDialog(
114 | new builder.Message(session)
115 | .attachments(tiles)
116 | .attachmentLayout(builder.AttachmentLayout.carousel)
117 | );
118 | });
119 | };
120 | */
121 |
122 | module.exports = function(bot) {
123 | bot.dialog('/addToCart', [
124 | function(session, args, next) {
125 | if (!args) {
126 | return session.reset('/confused');
127 | }
128 |
129 | const id = builder.EntityRecognizer.findEntity(args.entities, 'Id');
130 | if (!id || !id.entity) {
131 | return session.reset('/confused');
132 | }
133 |
134 | lookupProductOrVariant(session, id.entity, next)
135 | .then(({ product, variant }) => next({ product, variant }))
136 | .catch(error => console.error(error));
137 | },
138 | function(session, args, next) {
139 | const product = args.product;
140 | const variant = args.variant;
141 |
142 | session.privateConversationData.cart = (
143 | session.privateConversationData.cart || []
144 | ).concat({
145 | product,
146 | variant
147 | });
148 |
149 | session.send(`I have added ${describe(product, variant)} to your cart`);
150 |
151 | next({ variant });
152 | },
153 | function(session, args, next) {
154 | // not doing recommendations at the moment
155 | session.reset('/showCart');
156 | }
157 | /*
158 | Not used at the moment.
159 | Microsoft decided to discontinue the Recommendations API preview and the alternative is not exactly the same.
160 | I may come back later and integrate another service
161 | */
162 | /*
163 | function(session, args, next) {
164 | session.sendTyping();
165 |
166 | recommendations.recommend([args.variant.sku]).then(variants => {
167 | session.sendTyping();
168 |
169 | if (!variants.length) {
170 | session.reset('/showCart');
171 | } else {
172 | session.dialogData.recommendations = variants;
173 | next();
174 | }
175 | });
176 | },
177 | ...sentiment.confirm(
178 | 'I also have a few recommendations for you, would you like to see them?'
179 | ),
180 | function(session, args, next) {
181 | if (!args.response) {
182 | session.endDialog(
183 | 'Alright. Let me know if I can help you find anything else or if you would like to see your shopping cart.'
184 | );
185 | } else {
186 | showRecommendations(session);
187 | }
188 | }
189 | */
190 | ]);
191 | };
192 |
--------------------------------------------------------------------------------
/app/dialogs/explore.js:
--------------------------------------------------------------------------------
1 | const builder = require('botbuilder');
2 | const search = require('../search/search');
3 |
4 | const extractQuery = (session, args) => {
5 | if (args && args.entities && args.entities.length) {
6 | // builder.EntityRecognizer.findEntity(args.entities, 'CompanyName');
7 | // builder.EntityRecognizer.findBestMatch(data, entity.entity);
8 | const question = args.entities.find(e => e.type === 'Entity');
9 | const detail = args.entities.find(e => e.type === 'Detail');
10 |
11 | return `${(detail || { entity: '' }).entity} ${
12 | (question || { entity: '' }).entity
13 | }`.trim();
14 | } else if (session.message.text.split(' ').length <= 2) {
15 | // just assume they typed a category or a product name
16 | return session.message.text.replace('please', '').trim();
17 | } else {
18 | return undefined;
19 | }
20 | };
21 |
22 | const listCategories = (session, subcategories, start = 0) => {
23 | // ToDo: to be saved as a pagination object on the state and the list needs to be saved too
24 | const slice = subcategories.slice(start, start + 6);
25 | if (slice.length === 0) {
26 | return session.endDialog(
27 | "That's it. You have seen it all. See anything you like? Just ask for it."
28 | );
29 | }
30 |
31 | // ToDo: I have two displays. Cards and words. probably need a method to present it
32 | const message = slice.map(c => c.title).join(', ');
33 | const more = start + slice.length < subcategories.length;
34 |
35 | if (!more) {
36 | session.endDialog(
37 | `We ${
38 | start > 0 ? 'also ' : ''
39 | }have ${message}. See anything you like? Just ask for it.`
40 | );
41 | } else {
42 | session.endDialog(
43 | `We ${start > 0 ? 'also ' : ''}have ${message} and ${
44 | start > 0 ? 'even ' : ''
45 | }more.` +
46 | (start > 0
47 | ? " Keep scrolling if you don't see what you like."
48 | : ' You can scroll through the list with "next" or "more"')
49 | );
50 | }
51 | };
52 |
53 | const listProducts = (session, products, start = 0) => {
54 | // ToDo: need to filter out products with very small @search.score
55 | const slice = products.slice(start, start + 4);
56 | if (slice.length === 0) {
57 | return session.endDialog(
58 | "That's it. You have seen it all. See anything you like? Just ask for it."
59 | );
60 | }
61 |
62 | const cards = slice.map(p =>
63 | new builder.ThumbnailCard(session)
64 | .title(p.title)
65 | .subtitle(`$${p.price}`)
66 | .text(p.description)
67 | .buttons([
68 | builder.CardAction.postBack(session, `@show:${p.id}`, 'Show me')
69 | ])
70 | .images([
71 | builder.CardImage.create(session, p.image).tap(
72 | builder.CardAction.postBack(session, `@show:${p.id}`)
73 | )
74 | ])
75 | );
76 |
77 | if (start === 0) {
78 | session.send(
79 | `I found ${
80 | products.length
81 | } products and here are the best matches. Tap on the image to take a closer look.`
82 | );
83 | }
84 |
85 | session.sendTyping();
86 | session.endDialog(
87 | new builder.Message(session)
88 | .attachments(cards)
89 | .attachmentLayout(builder.AttachmentLayout.list)
90 | );
91 | };
92 |
93 | module.exports = function(bot) {
94 | bot.dialog('/explore', [
95 | function(session, args, next) {
96 | const query = extractQuery(session, args);
97 |
98 | if (!query) {
99 | // ToDo: randomize across a few different sentences
100 | builder.Prompts.text(
101 | session,
102 | 'I am sorry, what would you like me to look up for you?'
103 | );
104 | } else {
105 | next({ response: query });
106 | }
107 | },
108 | function(session, args, next) {
109 | session.sendTyping();
110 |
111 | const query = args.response;
112 |
113 | // ToDo: also need to search for products in the category
114 | search.find(query).then(({ subcategories, products }) => {
115 | if (subcategories.length) {
116 | session.privateConversationData = Object.assign(
117 | {},
118 | session.privateConversationData,
119 | {
120 | list: {
121 | type: 'categories',
122 | data: subcategories
123 | },
124 | pagination: {
125 | start: 0
126 | }
127 | }
128 | );
129 | session.save();
130 |
131 | listCategories(session, subcategories);
132 | } else if (products.length) {
133 | session.privateConversationData = Object.assign(
134 | {},
135 | session.privateConversationData,
136 | {
137 | list: {
138 | type: 'products',
139 | data: products
140 | },
141 | pagination: {
142 | start: 0
143 | }
144 | }
145 | );
146 | session.save();
147 |
148 | listProducts(session, products);
149 | } else {
150 | session.endDialog(
151 | `I tried looking for ${query} but I couldn't find anything, sorry!`
152 | );
153 | }
154 | });
155 | }
156 | ]);
157 |
158 | bot.dialog('/next', [
159 | function(session, args, next) {
160 | if (
161 | !session.privateConversationData ||
162 | !session.privateConversationData.list
163 | ) {
164 | return session.endDialog('Sorry, I have no active list to scroll');
165 | }
166 |
167 | const list = session.privateConversationData.list;
168 | const pagination = session.privateConversationData.pagination;
169 |
170 | switch (list.type) {
171 | case 'products':
172 | session.privateConversationData = Object.assign(
173 | {},
174 | session.privateConversationData,
175 | {
176 | pagination: {
177 | start: pagination.start + 4
178 | }
179 | }
180 | );
181 | session.save();
182 |
183 | return listProducts(session, list.data, pagination.start + 4);
184 |
185 | case 'categories':
186 | // ToDo: this is updating the state. Time to use Redux maybe?
187 | session.privateConversationData = Object.assign(
188 | {},
189 | session.privateConversationData,
190 | {
191 | pagination: {
192 | start: pagination.start + 6
193 | }
194 | }
195 | );
196 | session.save();
197 |
198 | return listCategories(session, list.data, pagination.start + 6);
199 | }
200 |
201 | session.endDialog(
202 | 'Something funny happened and I started wondering who I am'
203 | );
204 | }
205 | ]);
206 | };
207 |
--------------------------------------------------------------------------------
/indexes/populate.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const _ = require('lodash');
6 | const request = require('request-promise-native');
7 |
8 | const MoltinGateway = require('@moltin/sdk').gateway;
9 | const Moltin = MoltinGateway({
10 | client_id: process.env.MOLTIN_CLIENT_ID,
11 | client_secret: process.env.MOLTIN_CLIENT_SECRET
12 | });
13 |
14 | if (!Moltin.Files) {
15 | // Moltin JS SDK does not support files operation in 3.3.0
16 | Moltin.Files = Object.setPrototypeOf(
17 | Object.assign({}, Moltin.Products),
18 | Moltin.Products
19 | );
20 | Moltin.Files.endpoint = 'files';
21 | }
22 |
23 | (async function() {
24 | // There are only 42 images in the AW catalog and the default pagination limit in Moltin API is 100
25 | const images = (await Moltin.Files.All()).data;
26 | // A quick a way to pull up an image by id
27 | const imagesLookup = _.groupBy(images, image => image.id);
28 |
29 | // Tree reads all the categories in one go
30 | const taxonomy = (await Moltin.Categories.Tree()).data;
31 | for (let topCategory of taxonomy) {
32 | for (let child of topCategory.children) {
33 | child.parent = topCategory;
34 | }
35 | }
36 |
37 | // In AW, products link to sub-categories, not the top level categories
38 | const categories = _.flatMap(taxonomy, category => category.children || []);
39 | // A quick way to pull up a category by id
40 | const categoryLookup = _.groupBy(categories, category => category.id);
41 |
42 | // Need to recursively read all products
43 | const catalog = await (async function read(offset = 0, all = []) {
44 | Moltin.Products.Offset(offset);
45 | const { data, meta } = await Moltin.Products.All();
46 |
47 | all.push(...data);
48 |
49 | const total = meta.results.all;
50 | const processed =
51 | (meta.page.current - 1) * meta.page.limit + meta.results.total;
52 |
53 | return total > processed ? await read(processed, all) : all;
54 | })();
55 |
56 | // Top level products were all created with a generated sku number
57 | // actual SKUs that can be purchased are all on the variants
58 | const allProducts = catalog.filter(record => /^AW_\d+$/.test(record.sku));
59 | const allVariants = catalog.filter(record => !/^AW_\d+$/.test(record.sku));
60 |
61 | for (let variant of allVariants) {
62 | // When we load Adventure Works to Moltin, we give variants
63 | // JSON metadata indicating what color and size this variant represents
64 | // and also what product is the parent product for this variant
65 | variant.description = JSON.parse(variant.description);
66 | }
67 | // A quick way to pull up a list of product's variants
68 | const variantsLookup = _.groupBy(allVariants, v => v.description.parent);
69 |
70 | console.log(`Collecting data for the categories index`);
71 |
72 | const categoryIndex = taxonomy.concat(categories).map(category => ({
73 | '@search.action': 'upload',
74 | id: category.id,
75 | title: category.name,
76 | description: category.description,
77 | parent: category.parent ? category.parent.id : null
78 | }));
79 |
80 | console.log(`Collecting data for the products index`);
81 |
82 | const productIndex = allProducts.map(product => {
83 | const categoryId = product.relationships.categories.data[0].id;
84 |
85 | const category = categoryLookup[categoryId][0];
86 | const variants = variantsLookup[product.id];
87 |
88 | const modifiers = _.chain(variants)
89 | .flatMap(variant =>
90 | _.without(Object.keys(variant.description), 'parent').filter(key =>
91 | Boolean(variant.description[key])
92 | )
93 | )
94 | .uniq()
95 | .value();
96 |
97 | const [color, size] = ['color', 'size'].map(modifier =>
98 | _.chain(variants)
99 | .map(variant => variant.description[modifier])
100 | .uniq()
101 | .filter(Boolean)
102 | .value()
103 | );
104 |
105 | const image = imagesLookup[product.relationships.main_image.data.id][0];
106 |
107 | return {
108 | '@search.action': 'upload',
109 | id: product.id,
110 | title: product.name,
111 | description: product.description,
112 | category: category.parent.name,
113 | categoryId: category.parent.id,
114 | subcategory: category.name,
115 | subcategoryId: category.id,
116 | modifiers: modifiers,
117 | color: color, // ToDo: check how empty arrays are created in Azure Search
118 | size: size,
119 | price: Number(product.price[0].amount),
120 | image: image.link.href
121 | };
122 | });
123 |
124 | console.log(`Collecting data for the variants index`);
125 |
126 | // ToDo: double check that products without modifiers (no variations relations)
127 | // actually have their single variant created in Moltin
128 |
129 | const variantIndex = allVariants.map(variant => {
130 | const [color, size] = ['color', 'size'].map(
131 | modifier => variant.description[modifier] || null
132 | );
133 |
134 | const image = imagesLookup[variant.relationships.main_image.data.id][0];
135 |
136 | return {
137 | '@search.action': 'upload',
138 | id: variant.id,
139 | productId: variant.description.parent,
140 | color: color,
141 | size: size,
142 | sku: variant.sku,
143 | price: Number(variant.price[0].amount),
144 | image: image.link.href
145 | };
146 | });
147 |
148 | const indexes = {
149 | categories: categoryIndex,
150 | products: productIndex,
151 | variants: variantIndex
152 | };
153 |
154 | const servicename = process.env.SEARCH_APP_NAME;
155 | const apikey = process.env.SEARCH_API_KEY;
156 | const headers = {
157 | 'Content-Type': 'application/json',
158 | 'api-key': apikey
159 | };
160 |
161 | for (let index of Object.keys(indexes)) {
162 | console.log('Deleting %s index in Azure Search', index);
163 | try {
164 | await request({
165 | url: `https://${servicename}.search.windows.net/indexes/${index}?api-version=2016-09-01`,
166 | headers,
167 | method: 'DELETE'
168 | });
169 | } catch (error) {
170 | console.error(error);
171 | }
172 |
173 | console.log('(Re)creating %s index in Azure Search', index);
174 | await request({
175 | url: `https://${servicename}.search.windows.net/indexes/${index}?api-version=2016-09-01`,
176 | headers,
177 | method: 'PUT',
178 | body: fs.createReadStream(path.resolve(__dirname, `${index}.json`))
179 | });
180 |
181 | console.log('Loading data for %s index in Azure Search', index);
182 | await request({
183 | url: `https://${servicename}.search.windows.net/indexes/${index}/docs/index?api-version=2016-09-01`,
184 | headers,
185 | method: 'POST',
186 | json: true,
187 | body: {
188 | value: indexes[index]
189 | }
190 | });
191 | }
192 |
193 | console.log('All said and done');
194 | })();
195 |
--------------------------------------------------------------------------------
/luis/commercehcat.json:
--------------------------------------------------------------------------------
1 | {
2 | "luis_schema_version": "2.1.0",
3 | "versionId": "0.1",
4 | "name": "commercehcat",
5 | "desc": "",
6 | "culture": "en-us",
7 | "intents": [
8 | {
9 | "name": "Checkout"
10 | },
11 | {
12 | "name": "Explore"
13 | },
14 | {
15 | "name": "None"
16 | },
17 | {
18 | "name": "ShowCart"
19 | },
20 | {
21 | "name": "ShowTopCategories"
22 | }
23 | ],
24 | "entities": [
25 | {
26 | "name": "Detail"
27 | },
28 | {
29 | "name": "Entity"
30 | }
31 | ],
32 | "composites": [],
33 | "closedLists": [],
34 | "bing_entities": [],
35 | "model_features": [],
36 | "regex_features": [],
37 | "utterances": [
38 | {
39 | "text": "checkout",
40 | "intent": "Checkout",
41 | "entities": []
42 | },
43 | {
44 | "text": "what kind of bikes do you have?",
45 | "intent": "Explore",
46 | "entities": [
47 | {
48 | "entity": "Entity",
49 | "startPos": 13,
50 | "endPos": 17
51 | }
52 | ]
53 | },
54 | {
55 | "text": "what do you have?",
56 | "intent": "ShowTopCategories",
57 | "entities": []
58 | },
59 | {
60 | "text": "what have you got?",
61 | "intent": "ShowTopCategories",
62 | "entities": []
63 | },
64 | {
65 | "text": "show me mountain bikes",
66 | "intent": "Explore",
67 | "entities": [
68 | {
69 | "entity": "Detail",
70 | "startPos": 8,
71 | "endPos": 15
72 | },
73 | {
74 | "entity": "Entity",
75 | "startPos": 17,
76 | "endPos": 21
77 | }
78 | ]
79 | },
80 | {
81 | "text": "show me what you got",
82 | "intent": "ShowTopCategories",
83 | "entities": []
84 | },
85 | {
86 | "text": "what do you have",
87 | "intent": "ShowTopCategories",
88 | "entities": []
89 | },
90 | {
91 | "text": "checkout please",
92 | "intent": "Checkout",
93 | "entities": []
94 | },
95 | {
96 | "text": "what's in my cart",
97 | "intent": "ShowCart",
98 | "entities": []
99 | },
100 | {
101 | "text": "show me some gloves",
102 | "intent": "Explore",
103 | "entities": [
104 | {
105 | "entity": "Entity",
106 | "startPos": 13,
107 | "endPos": 18
108 | }
109 | ]
110 | },
111 | {
112 | "text": "please show me gloves",
113 | "intent": "Explore",
114 | "entities": [
115 | {
116 | "entity": "Entity",
117 | "startPos": 15,
118 | "endPos": 20
119 | }
120 | ]
121 | },
122 | {
123 | "text": "do you have shirts?",
124 | "intent": "Explore",
125 | "entities": [
126 | {
127 | "entity": "Entity",
128 | "startPos": 12,
129 | "endPos": 17
130 | }
131 | ]
132 | },
133 | {
134 | "text": "what's in store?",
135 | "intent": "ShowTopCategories",
136 | "entities": []
137 | },
138 | {
139 | "text": "show me the bikes",
140 | "intent": "Explore",
141 | "entities": [
142 | {
143 | "entity": "Entity",
144 | "startPos": 12,
145 | "endPos": 16
146 | }
147 | ]
148 | },
149 | {
150 | "text": "i would like to see gloves",
151 | "intent": "Explore",
152 | "entities": [
153 | {
154 | "entity": "Entity",
155 | "startPos": 20,
156 | "endPos": 25
157 | }
158 | ]
159 | },
160 | {
161 | "text": "what bikes do you have?",
162 | "intent": "Explore",
163 | "entities": [
164 | {
165 | "entity": "Entity",
166 | "startPos": 5,
167 | "endPos": 9
168 | }
169 | ]
170 | },
171 | {
172 | "text": "what kind of touring bikes?",
173 | "intent": "Explore",
174 | "entities": [
175 | {
176 | "entity": "Detail",
177 | "startPos": 13,
178 | "endPos": 19
179 | },
180 | {
181 | "entity": "Entity",
182 | "startPos": 21,
183 | "endPos": 25
184 | }
185 | ]
186 | },
187 | {
188 | "text": "do you have touring bikes?",
189 | "intent": "Explore",
190 | "entities": [
191 | {
192 | "entity": "Detail",
193 | "startPos": 12,
194 | "endPos": 18
195 | },
196 | {
197 | "entity": "Entity",
198 | "startPos": 20,
199 | "endPos": 24
200 | }
201 | ]
202 | },
203 | {
204 | "text": "show me bikes please",
205 | "intent": "Explore",
206 | "entities": [
207 | {
208 | "entity": "Entity",
209 | "startPos": 8,
210 | "endPos": 12
211 | }
212 | ]
213 | },
214 | {
215 | "text": "show me touring bikes please",
216 | "intent": "Explore",
217 | "entities": [
218 | {
219 | "entity": "Detail",
220 | "startPos": 8,
221 | "endPos": 14
222 | },
223 | {
224 | "entity": "Entity",
225 | "startPos": 16,
226 | "endPos": 20
227 | }
228 | ]
229 | },
230 | {
231 | "text": "show me around please",
232 | "intent": "ShowTopCategories",
233 | "entities": []
234 | },
235 | {
236 | "text": "alright. tell me what you have?",
237 | "intent": "ShowTopCategories",
238 | "entities": []
239 | },
240 | {
241 | "text": "alright. so what do you have?",
242 | "intent": "ShowTopCategories",
243 | "entities": []
244 | },
245 | {
246 | "text": "do you have mountain bikes?",
247 | "intent": "Explore",
248 | "entities": [
249 | {
250 | "entity": "Detail",
251 | "startPos": 12,
252 | "endPos": 19
253 | },
254 | {
255 | "entity": "Entity",
256 | "startPos": 21,
257 | "endPos": 25
258 | }
259 | ]
260 | },
261 | {
262 | "text": "what kind of accessories?",
263 | "intent": "Explore",
264 | "entities": [
265 | {
266 | "entity": "Entity",
267 | "startPos": 13,
268 | "endPos": 23
269 | }
270 | ]
271 | },
272 | {
273 | "text": "what helmets?",
274 | "intent": "Explore",
275 | "entities": [
276 | {
277 | "entity": "Entity",
278 | "startPos": 5,
279 | "endPos": 11
280 | }
281 | ]
282 | },
283 | {
284 | "text": "can you show me helmets?",
285 | "intent": "Explore",
286 | "entities": [
287 | {
288 | "entity": "Entity",
289 | "startPos": 16,
290 | "endPos": 22
291 | }
292 | ]
293 | },
294 | {
295 | "text": "i need some handlebars",
296 | "intent": "Explore",
297 | "entities": [
298 | {
299 | "entity": "Entity",
300 | "startPos": 12,
301 | "endPos": 21
302 | }
303 | ]
304 | },
305 | {
306 | "text": "what kind of bike racks do you have?",
307 | "intent": "Explore",
308 | "entities": [
309 | {
310 | "entity": "Detail",
311 | "startPos": 13,
312 | "endPos": 16
313 | },
314 | {
315 | "entity": "Entity",
316 | "startPos": 18,
317 | "endPos": 22
318 | }
319 | ]
320 | },
321 | {
322 | "text": "can you show me the bikes?",
323 | "intent": "Explore",
324 | "entities": [
325 | {
326 | "entity": "Entity",
327 | "startPos": 20,
328 | "endPos": 24
329 | }
330 | ]
331 | },
332 | {
333 | "text": "please show me bottom brackets",
334 | "intent": "Explore",
335 | "entities": [
336 | {
337 | "entity": "Detail",
338 | "startPos": 15,
339 | "endPos": 20
340 | },
341 | {
342 | "entity": "Entity",
343 | "startPos": 22,
344 | "endPos": 29
345 | }
346 | ]
347 | },
348 | {
349 | "text": "i need some bib-shorts",
350 | "intent": "Explore",
351 | "entities": [
352 | {
353 | "entity": "Detail",
354 | "startPos": 12,
355 | "endPos": 14
356 | },
357 | {
358 | "entity": "Entity",
359 | "startPos": 16,
360 | "endPos": 21
361 | }
362 | ]
363 | },
364 | {
365 | "text": "what kind of handlebars do you have?",
366 | "intent": "Explore",
367 | "entities": [
368 | {
369 | "entity": "Entity",
370 | "startPos": 13,
371 | "endPos": 22
372 | }
373 | ]
374 | },
375 | {
376 | "text": "please show me pedals",
377 | "intent": "Explore",
378 | "entities": [
379 | {
380 | "entity": "Entity",
381 | "startPos": 15,
382 | "endPos": 20
383 | }
384 | ]
385 | },
386 | {
387 | "text": "show me some derailleurs",
388 | "intent": "Explore",
389 | "entities": [
390 | {
391 | "entity": "Entity",
392 | "startPos": 13,
393 | "endPos": 23
394 | }
395 | ]
396 | },
397 | {
398 | "text": "i would like to see road frames",
399 | "intent": "Explore",
400 | "entities": [
401 | {
402 | "entity": "Detail",
403 | "startPos": 20,
404 | "endPos": 23
405 | },
406 | {
407 | "entity": "Entity",
408 | "startPos": 25,
409 | "endPos": 30
410 | }
411 | ]
412 | },
413 | {
414 | "text": "i need some cleaners",
415 | "intent": "Explore",
416 | "entities": [
417 | {
418 | "entity": "Entity",
419 | "startPos": 12,
420 | "endPos": 19
421 | }
422 | ]
423 | },
424 | {
425 | "text": "interested in saddles",
426 | "intent": "Explore",
427 | "entities": [
428 | {
429 | "entity": "Entity",
430 | "startPos": 14,
431 | "endPos": 20
432 | }
433 | ]
434 | },
435 | {
436 | "text": "interested in tights",
437 | "intent": "Explore",
438 | "entities": [
439 | {
440 | "entity": "Entity",
441 | "startPos": 14,
442 | "endPos": 19
443 | }
444 | ]
445 | },
446 | {
447 | "text": "i would like to see saddles",
448 | "intent": "Explore",
449 | "entities": [
450 | {
451 | "entity": "Entity",
452 | "startPos": 20,
453 | "endPos": 26
454 | }
455 | ]
456 | },
457 | {
458 | "text": "i need some bike racks",
459 | "intent": "Explore",
460 | "entities": [
461 | {
462 | "entity": "Detail",
463 | "startPos": 12,
464 | "endPos": 15
465 | },
466 | {
467 | "entity": "Entity",
468 | "startPos": 17,
469 | "endPos": 21
470 | }
471 | ]
472 | },
473 | {
474 | "text": "i need some bottom brackets",
475 | "intent": "Explore",
476 | "entities": [
477 | {
478 | "entity": "Detail",
479 | "startPos": 12,
480 | "endPos": 17
481 | },
482 | {
483 | "entity": "Entity",
484 | "startPos": 19,
485 | "endPos": 26
486 | }
487 | ]
488 | },
489 | {
490 | "text": "i need some mountain bikes",
491 | "intent": "Explore",
492 | "entities": [
493 | {
494 | "entity": "Detail",
495 | "startPos": 12,
496 | "endPos": 19
497 | },
498 | {
499 | "entity": "Entity",
500 | "startPos": 21,
501 | "endPos": 25
502 | }
503 | ]
504 | },
505 | {
506 | "text": "i need some touring bikes",
507 | "intent": "Explore",
508 | "entities": [
509 | {
510 | "entity": "Detail",
511 | "startPos": 12,
512 | "endPos": 18
513 | },
514 | {
515 | "entity": "Entity",
516 | "startPos": 20,
517 | "endPos": 24
518 | }
519 | ]
520 | },
521 | {
522 | "text": "i would like to see bike racks",
523 | "intent": "Explore",
524 | "entities": [
525 | {
526 | "entity": "Detail",
527 | "startPos": 20,
528 | "endPos": 23
529 | },
530 | {
531 | "entity": "Entity",
532 | "startPos": 25,
533 | "endPos": 29
534 | }
535 | ]
536 | },
537 | {
538 | "text": "i would like to see mountain frames",
539 | "intent": "Explore",
540 | "entities": [
541 | {
542 | "entity": "Detail",
543 | "startPos": 20,
544 | "endPos": 27
545 | },
546 | {
547 | "entity": "Entity",
548 | "startPos": 29,
549 | "endPos": 34
550 | }
551 | ]
552 | },
553 | {
554 | "text": "i would like to see bib-shorts",
555 | "intent": "Explore",
556 | "entities": [
557 | {
558 | "entity": "Detail",
559 | "startPos": 20,
560 | "endPos": 22
561 | },
562 | {
563 | "entity": "Entity",
564 | "startPos": 24,
565 | "endPos": 29
566 | }
567 | ]
568 | },
569 | {
570 | "text": "i would like to see bottom brackets",
571 | "intent": "Explore",
572 | "entities": [
573 | {
574 | "entity": "Detail",
575 | "startPos": 20,
576 | "endPos": 25
577 | },
578 | {
579 | "entity": "Entity",
580 | "startPos": 27,
581 | "endPos": 34
582 | }
583 | ]
584 | },
585 | {
586 | "text": "i would like to see touring frames",
587 | "intent": "Explore",
588 | "entities": [
589 | {
590 | "entity": "Detail",
591 | "startPos": 20,
592 | "endPos": 26
593 | },
594 | {
595 | "entity": "Entity",
596 | "startPos": 28,
597 | "endPos": 33
598 | }
599 | ]
600 | },
601 | {
602 | "text": "i would like to see road bikes",
603 | "intent": "Explore",
604 | "entities": [
605 | {
606 | "entity": "Detail",
607 | "startPos": 20,
608 | "endPos": 23
609 | },
610 | {
611 | "entity": "Entity",
612 | "startPos": 25,
613 | "endPos": 29
614 | }
615 | ]
616 | },
617 | {
618 | "text": "what kind of bottom brackets do you have?",
619 | "intent": "Explore",
620 | "entities": [
621 | {
622 | "entity": "Detail",
623 | "startPos": 13,
624 | "endPos": 18
625 | },
626 | {
627 | "entity": "Entity",
628 | "startPos": 20,
629 | "endPos": 27
630 | }
631 | ]
632 | },
633 | {
634 | "text": "what kind of touring frames do you have?",
635 | "intent": "Explore",
636 | "entities": [
637 | {
638 | "entity": "Detail",
639 | "startPos": 13,
640 | "endPos": 19
641 | },
642 | {
643 | "entity": "Entity",
644 | "startPos": 21,
645 | "endPos": 26
646 | }
647 | ]
648 | },
649 | {
650 | "text": "i would like to see mountain bikes",
651 | "intent": "Explore",
652 | "entities": [
653 | {
654 | "entity": "Detail",
655 | "startPos": 20,
656 | "endPos": 27
657 | },
658 | {
659 | "entity": "Entity",
660 | "startPos": 29,
661 | "endPos": 33
662 | }
663 | ]
664 | },
665 | {
666 | "text": "i would like to see touring bikes",
667 | "intent": "Explore",
668 | "entities": [
669 | {
670 | "entity": "Detail",
671 | "startPos": 20,
672 | "endPos": 26
673 | },
674 | {
675 | "entity": "Entity",
676 | "startPos": 28,
677 | "endPos": 32
678 | }
679 | ]
680 | },
681 | {
682 | "text": "i would like to see tires and tubes",
683 | "intent": "Explore",
684 | "entities": [
685 | {
686 | "entity": "Entity",
687 | "startPos": 20,
688 | "endPos": 24
689 | },
690 | {
691 | "entity": "Entity",
692 | "startPos": 30,
693 | "endPos": 34
694 | }
695 | ]
696 | },
697 | {
698 | "text": "please show me bottles and cages",
699 | "intent": "Explore",
700 | "entities": [
701 | {
702 | "entity": "Entity",
703 | "startPos": 15,
704 | "endPos": 21
705 | },
706 | {
707 | "entity": "Entity",
708 | "startPos": 27,
709 | "endPos": 31
710 | }
711 | ]
712 | },
713 | {
714 | "text": "i would like to see hydration packs",
715 | "intent": "Explore",
716 | "entities": [
717 | {
718 | "entity": "Detail",
719 | "startPos": 20,
720 | "endPos": 28
721 | },
722 | {
723 | "entity": "Entity",
724 | "startPos": 30,
725 | "endPos": 34
726 | }
727 | ]
728 | },
729 | {
730 | "text": "what kind of bike stands do you have?",
731 | "intent": "Explore",
732 | "entities": [
733 | {
734 | "entity": "Detail",
735 | "startPos": 13,
736 | "endPos": 16
737 | },
738 | {
739 | "entity": "Entity",
740 | "startPos": 18,
741 | "endPos": 23
742 | }
743 | ]
744 | },
745 | {
746 | "text": "interested in tires and tubes",
747 | "intent": "Explore",
748 | "entities": [
749 | {
750 | "entity": "Entity",
751 | "startPos": 14,
752 | "endPos": 18
753 | },
754 | {
755 | "entity": "Entity",
756 | "startPos": 24,
757 | "endPos": 28
758 | }
759 | ]
760 | },
761 | {
762 | "text": "i would like to see bottles and cages",
763 | "intent": "Explore",
764 | "entities": [
765 | {
766 | "entity": "Entity",
767 | "startPos": 20,
768 | "endPos": 26
769 | },
770 | {
771 | "entity": "Entity",
772 | "startPos": 32,
773 | "endPos": 36
774 | }
775 | ]
776 | },
777 | {
778 | "text": "i would like to see caps",
779 | "intent": "Explore",
780 | "entities": [
781 | {
782 | "entity": "Entity",
783 | "startPos": 20,
784 | "endPos": 23
785 | }
786 | ]
787 | },
788 | {
789 | "text": "please show me hydration packs",
790 | "intent": "Explore",
791 | "entities": [
792 | {
793 | "entity": "Detail",
794 | "startPos": 15,
795 | "endPos": 23
796 | },
797 | {
798 | "entity": "Entity",
799 | "startPos": 25,
800 | "endPos": 29
801 | }
802 | ]
803 | },
804 | {
805 | "text": "i would like to see jerseys",
806 | "intent": "Explore",
807 | "entities": [
808 | {
809 | "entity": "Entity",
810 | "startPos": 20,
811 | "endPos": 26
812 | }
813 | ]
814 | },
815 | {
816 | "text": "interested in fenders",
817 | "intent": "Explore",
818 | "entities": [
819 | {
820 | "entity": "Entity",
821 | "startPos": 14,
822 | "endPos": 20
823 | }
824 | ]
825 | },
826 | {
827 | "text": "what kind of fenders do you have?",
828 | "intent": "Explore",
829 | "entities": [
830 | {
831 | "entity": "Entity",
832 | "startPos": 13,
833 | "endPos": 19
834 | }
835 | ]
836 | },
837 | {
838 | "text": "what kind of vests do you have?",
839 | "intent": "Explore",
840 | "entities": [
841 | {
842 | "entity": "Entity",
843 | "startPos": 13,
844 | "endPos": 17
845 | }
846 | ]
847 | },
848 | {
849 | "text": "please show me socks",
850 | "intent": "Explore",
851 | "entities": [
852 | {
853 | "entity": "Entity",
854 | "startPos": 15,
855 | "endPos": 19
856 | }
857 | ]
858 | },
859 | {
860 | "text": "please show me brakes",
861 | "intent": "Explore",
862 | "entities": [
863 | {
864 | "entity": "Entity",
865 | "startPos": 15,
866 | "endPos": 20
867 | }
868 | ]
869 | },
870 | {
871 | "text": "please show me locks",
872 | "intent": "Explore",
873 | "entities": [
874 | {
875 | "entity": "Entity",
876 | "startPos": 15,
877 | "endPos": 19
878 | }
879 | ]
880 | },
881 | {
882 | "text": "i need some gloves",
883 | "intent": "Explore",
884 | "entities": [
885 | {
886 | "entity": "Entity",
887 | "startPos": 12,
888 | "endPos": 17
889 | }
890 | ]
891 | },
892 | {
893 | "text": "i would like to see headsets",
894 | "intent": "Explore",
895 | "entities": [
896 | {
897 | "entity": "Entity",
898 | "startPos": 20,
899 | "endPos": 27
900 | }
901 | ]
902 | },
903 | {
904 | "text": "please show me chains",
905 | "intent": "Explore",
906 | "entities": [
907 | {
908 | "entity": "Entity",
909 | "startPos": 15,
910 | "endPos": 20
911 | }
912 | ]
913 | },
914 | {
915 | "text": "i would like to see pumps",
916 | "intent": "Explore",
917 | "entities": [
918 | {
919 | "entity": "Entity",
920 | "startPos": 20,
921 | "endPos": 24
922 | }
923 | ]
924 | },
925 | {
926 | "text": "i need some forks",
927 | "intent": "Explore",
928 | "entities": [
929 | {
930 | "entity": "Entity",
931 | "startPos": 12,
932 | "endPos": 16
933 | }
934 | ]
935 | },
936 | {
937 | "text": "i would like to see wheels",
938 | "intent": "Explore",
939 | "entities": [
940 | {
941 | "entity": "Entity",
942 | "startPos": 20,
943 | "endPos": 25
944 | }
945 | ]
946 | },
947 | {
948 | "text": "i need some lights",
949 | "intent": "Explore",
950 | "entities": [
951 | {
952 | "entity": "Entity",
953 | "startPos": 12,
954 | "endPos": 17
955 | }
956 | ]
957 | },
958 | {
959 | "text": "interested in bike stands",
960 | "intent": "Explore",
961 | "entities": [
962 | {
963 | "entity": "Detail",
964 | "startPos": 14,
965 | "endPos": 17
966 | },
967 | {
968 | "entity": "Entity",
969 | "startPos": 19,
970 | "endPos": 24
971 | }
972 | ]
973 | },
974 | {
975 | "text": "what kind of bib-shorts do you have?",
976 | "intent": "Explore",
977 | "entities": [
978 | {
979 | "entity": "Detail",
980 | "startPos": 13,
981 | "endPos": 15
982 | },
983 | {
984 | "entity": "Entity",
985 | "startPos": 17,
986 | "endPos": 22
987 | }
988 | ]
989 | },
990 | {
991 | "text": "what kind of hydration packs do you have?",
992 | "intent": "Explore",
993 | "entities": [
994 | {
995 | "entity": "Detail",
996 | "startPos": 13,
997 | "endPos": 21
998 | },
999 | {
1000 | "entity": "Entity",
1001 | "startPos": 23,
1002 | "endPos": 27
1003 | }
1004 | ]
1005 | },
1006 | {
1007 | "text": "what kind of road frames do you have?",
1008 | "intent": "Explore",
1009 | "entities": [
1010 | {
1011 | "entity": "Detail",
1012 | "startPos": 13,
1013 | "endPos": 16
1014 | },
1015 | {
1016 | "entity": "Entity",
1017 | "startPos": 18,
1018 | "endPos": 23
1019 | }
1020 | ]
1021 | },
1022 | {
1023 | "text": "interested in bib-shorts",
1024 | "intent": "Explore",
1025 | "entities": [
1026 | {
1027 | "entity": "Detail",
1028 | "startPos": 14,
1029 | "endPos": 16
1030 | },
1031 | {
1032 | "entity": "Entity",
1033 | "startPos": 18,
1034 | "endPos": 23
1035 | }
1036 | ]
1037 | },
1038 | {
1039 | "text": "please show me bike stands",
1040 | "intent": "Explore",
1041 | "entities": [
1042 | {
1043 | "entity": "Detail",
1044 | "startPos": 15,
1045 | "endPos": 18
1046 | },
1047 | {
1048 | "entity": "Entity",
1049 | "startPos": 20,
1050 | "endPos": 25
1051 | }
1052 | ]
1053 | },
1054 | {
1055 | "text": "interested in vests",
1056 | "intent": "Explore",
1057 | "entities": [
1058 | {
1059 | "entity": "Entity",
1060 | "startPos": 14,
1061 | "endPos": 18
1062 | }
1063 | ]
1064 | },
1065 | {
1066 | "text": "i would like to see tights",
1067 | "intent": "Explore",
1068 | "entities": [
1069 | {
1070 | "entity": "Entity",
1071 | "startPos": 20,
1072 | "endPos": 25
1073 | }
1074 | ]
1075 | },
1076 | {
1077 | "text": "what kind of forks do you have?",
1078 | "intent": "Explore",
1079 | "entities": [
1080 | {
1081 | "entity": "Entity",
1082 | "startPos": 13,
1083 | "endPos": 17
1084 | }
1085 | ]
1086 | },
1087 | {
1088 | "text": "what kind of wheels do you have?",
1089 | "intent": "Explore",
1090 | "entities": [
1091 | {
1092 | "entity": "Entity",
1093 | "startPos": 13,
1094 | "endPos": 18
1095 | }
1096 | ]
1097 | },
1098 | {
1099 | "text": "what kind of lights do you have?",
1100 | "intent": "Explore",
1101 | "entities": [
1102 | {
1103 | "entity": "Entity",
1104 | "startPos": 13,
1105 | "endPos": 18
1106 | }
1107 | ]
1108 | },
1109 | {
1110 | "text": "please show me forks",
1111 | "intent": "Explore",
1112 | "entities": [
1113 | {
1114 | "entity": "Entity",
1115 | "startPos": 15,
1116 | "endPos": 19
1117 | }
1118 | ]
1119 | },
1120 | {
1121 | "text": "i would like to see bike stands",
1122 | "intent": "Explore",
1123 | "entities": [
1124 | {
1125 | "entity": "Detail",
1126 | "startPos": 20,
1127 | "endPos": 23
1128 | },
1129 | {
1130 | "entity": "Entity",
1131 | "startPos": 25,
1132 | "endPos": 30
1133 | }
1134 | ]
1135 | },
1136 | {
1137 | "text": "i need some socks",
1138 | "intent": "Explore",
1139 | "entities": [
1140 | {
1141 | "entity": "Entity",
1142 | "startPos": 12,
1143 | "endPos": 16
1144 | }
1145 | ]
1146 | },
1147 | {
1148 | "text": "i would like to see vests",
1149 | "intent": "Explore",
1150 | "entities": [
1151 | {
1152 | "entity": "Entity",
1153 | "startPos": 20,
1154 | "endPos": 24
1155 | }
1156 | ]
1157 | },
1158 | {
1159 | "text": "i would like to see cleaners",
1160 | "intent": "Explore",
1161 | "entities": [
1162 | {
1163 | "entity": "Entity",
1164 | "startPos": 20,
1165 | "endPos": 27
1166 | }
1167 | ]
1168 | },
1169 | {
1170 | "text": "interested in hydration packs",
1171 | "intent": "Explore",
1172 | "entities": [
1173 | {
1174 | "entity": "Detail",
1175 | "startPos": 14,
1176 | "endPos": 22
1177 | },
1178 | {
1179 | "entity": "Entity",
1180 | "startPos": 24,
1181 | "endPos": 28
1182 | }
1183 | ]
1184 | },
1185 | {
1186 | "text": "i need some brakes",
1187 | "intent": "Explore",
1188 | "entities": [
1189 | {
1190 | "entity": "Entity",
1191 | "startPos": 12,
1192 | "endPos": 17
1193 | }
1194 | ]
1195 | },
1196 | {
1197 | "text": "what kind of socks do you have?",
1198 | "intent": "Explore",
1199 | "entities": [
1200 | {
1201 | "entity": "Entity",
1202 | "startPos": 13,
1203 | "endPos": 17
1204 | }
1205 | ]
1206 | },
1207 | {
1208 | "text": "what kind of brakes do you have?",
1209 | "intent": "Explore",
1210 | "entities": [
1211 | {
1212 | "entity": "Entity",
1213 | "startPos": 13,
1214 | "endPos": 18
1215 | }
1216 | ]
1217 | },
1218 | {
1219 | "text": "what kind of locks do you have?",
1220 | "intent": "Explore",
1221 | "entities": [
1222 | {
1223 | "entity": "Entity",
1224 | "startPos": 13,
1225 | "endPos": 17
1226 | }
1227 | ]
1228 | },
1229 | {
1230 | "text": "what kind of chains do you have?",
1231 | "intent": "Explore",
1232 | "entities": [
1233 | {
1234 | "entity": "Entity",
1235 | "startPos": 13,
1236 | "endPos": 18
1237 | }
1238 | ]
1239 | },
1240 | {
1241 | "text": "i would like to see lights",
1242 | "intent": "Explore",
1243 | "entities": [
1244 | {
1245 | "entity": "Entity",
1246 | "startPos": 20,
1247 | "endPos": 25
1248 | }
1249 | ]
1250 | },
1251 | {
1252 | "text": "what kind of pedals do you have?",
1253 | "intent": "Explore",
1254 | "entities": [
1255 | {
1256 | "entity": "Entity",
1257 | "startPos": 13,
1258 | "endPos": 18
1259 | }
1260 | ]
1261 | },
1262 | {
1263 | "text": "i need some tights",
1264 | "intent": "Explore",
1265 | "entities": [
1266 | {
1267 | "entity": "Entity",
1268 | "startPos": 12,
1269 | "endPos": 17
1270 | }
1271 | ]
1272 | },
1273 | {
1274 | "text": "i would like to see forks",
1275 | "intent": "Explore",
1276 | "entities": [
1277 | {
1278 | "entity": "Entity",
1279 | "startPos": 20,
1280 | "endPos": 24
1281 | }
1282 | ]
1283 | },
1284 | {
1285 | "text": "show me some hydration packs",
1286 | "intent": "Explore",
1287 | "entities": [
1288 | {
1289 | "entity": "Detail",
1290 | "startPos": 13,
1291 | "endPos": 21
1292 | },
1293 | {
1294 | "entity": "Entity",
1295 | "startPos": 23,
1296 | "endPos": 27
1297 | }
1298 | ]
1299 | },
1300 | {
1301 | "text": "interested in caps",
1302 | "intent": "Explore",
1303 | "entities": [
1304 | {
1305 | "entity": "Entity",
1306 | "startPos": 14,
1307 | "endPos": 17
1308 | }
1309 | ]
1310 | },
1311 | {
1312 | "text": "i need some locks",
1313 | "intent": "Explore",
1314 | "entities": [
1315 | {
1316 | "entity": "Entity",
1317 | "startPos": 12,
1318 | "endPos": 16
1319 | }
1320 | ]
1321 | },
1322 | {
1323 | "text": "i would like to see brakes",
1324 | "intent": "Explore",
1325 | "entities": [
1326 | {
1327 | "entity": "Entity",
1328 | "startPos": 20,
1329 | "endPos": 25
1330 | }
1331 | ]
1332 | },
1333 | {
1334 | "text": "i need some chains",
1335 | "intent": "Explore",
1336 | "entities": [
1337 | {
1338 | "entity": "Entity",
1339 | "startPos": 12,
1340 | "endPos": 17
1341 | }
1342 | ]
1343 | },
1344 | {
1345 | "text": "interested in pumps",
1346 | "intent": "Explore",
1347 | "entities": [
1348 | {
1349 | "entity": "Entity",
1350 | "startPos": 14,
1351 | "endPos": 18
1352 | }
1353 | ]
1354 | },
1355 | {
1356 | "text": "what kind of mountain frames do you have?",
1357 | "intent": "Explore",
1358 | "entities": [
1359 | {
1360 | "entity": "Detail",
1361 | "startPos": 13,
1362 | "endPos": 20
1363 | },
1364 | {
1365 | "entity": "Entity",
1366 | "startPos": 22,
1367 | "endPos": 27
1368 | }
1369 | ]
1370 | },
1371 | {
1372 | "text": "i would like to see pedals",
1373 | "intent": "Explore",
1374 | "entities": [
1375 | {
1376 | "entity": "Entity",
1377 | "startPos": 20,
1378 | "endPos": 25
1379 | }
1380 | ]
1381 | },
1382 | {
1383 | "text": "interested in jerseys",
1384 | "intent": "Explore",
1385 | "entities": [
1386 | {
1387 | "entity": "Entity",
1388 | "startPos": 14,
1389 | "endPos": 20
1390 | }
1391 | ]
1392 | },
1393 | {
1394 | "text": "what kind of road bikes do you have?",
1395 | "intent": "Explore",
1396 | "entities": [
1397 | {
1398 | "entity": "Detail",
1399 | "startPos": 13,
1400 | "endPos": 16
1401 | },
1402 | {
1403 | "entity": "Entity",
1404 | "startPos": 18,
1405 | "endPos": 22
1406 | }
1407 | ]
1408 | },
1409 | {
1410 | "text": "please show me bib-shorts",
1411 | "intent": "Explore",
1412 | "entities": [
1413 | {
1414 | "entity": "Detail",
1415 | "startPos": 15,
1416 | "endPos": 17
1417 | },
1418 | {
1419 | "entity": "Entity",
1420 | "startPos": 19,
1421 | "endPos": 24
1422 | }
1423 | ]
1424 | },
1425 | {
1426 | "text": "i need some pumps",
1427 | "intent": "Explore",
1428 | "entities": [
1429 | {
1430 | "entity": "Entity",
1431 | "startPos": 12,
1432 | "endPos": 16
1433 | }
1434 | ]
1435 | },
1436 | {
1437 | "text": "i need some jerseys",
1438 | "intent": "Explore",
1439 | "entities": [
1440 | {
1441 | "entity": "Entity",
1442 | "startPos": 12,
1443 | "endPos": 18
1444 | }
1445 | ]
1446 | },
1447 | {
1448 | "text": "interested in headsets",
1449 | "intent": "Explore",
1450 | "entities": [
1451 | {
1452 | "entity": "Entity",
1453 | "startPos": 14,
1454 | "endPos": 21
1455 | }
1456 | ]
1457 | },
1458 | {
1459 | "text": "please show me road frames",
1460 | "intent": "Explore",
1461 | "entities": [
1462 | {
1463 | "entity": "Detail",
1464 | "startPos": 15,
1465 | "endPos": 18
1466 | },
1467 | {
1468 | "entity": "Entity",
1469 | "startPos": 20,
1470 | "endPos": 25
1471 | }
1472 | ]
1473 | },
1474 | {
1475 | "text": "please show me bike racks",
1476 | "intent": "Explore",
1477 | "entities": [
1478 | {
1479 | "entity": "Detail",
1480 | "startPos": 15,
1481 | "endPos": 18
1482 | },
1483 | {
1484 | "entity": "Entity",
1485 | "startPos": 20,
1486 | "endPos": 24
1487 | }
1488 | ]
1489 | },
1490 | {
1491 | "text": "show me some bottom brackets",
1492 | "intent": "Explore",
1493 | "entities": [
1494 | {
1495 | "entity": "Detail",
1496 | "startPos": 13,
1497 | "endPos": 18
1498 | },
1499 | {
1500 | "entity": "Entity",
1501 | "startPos": 20,
1502 | "endPos": 27
1503 | }
1504 | ]
1505 | },
1506 | {
1507 | "text": "i need some hydration packs",
1508 | "intent": "Explore",
1509 | "entities": [
1510 | {
1511 | "entity": "Detail",
1512 | "startPos": 12,
1513 | "endPos": 20
1514 | },
1515 | {
1516 | "entity": "Entity",
1517 | "startPos": 22,
1518 | "endPos": 26
1519 | }
1520 | ]
1521 | },
1522 | {
1523 | "text": "i would like to see locks",
1524 | "intent": "Explore",
1525 | "entities": [
1526 | {
1527 | "entity": "Entity",
1528 | "startPos": 20,
1529 | "endPos": 24
1530 | }
1531 | ]
1532 | },
1533 | {
1534 | "text": "i would like to see chains",
1535 | "intent": "Explore",
1536 | "entities": [
1537 | {
1538 | "entity": "Entity",
1539 | "startPos": 20,
1540 | "endPos": 25
1541 | }
1542 | ]
1543 | },
1544 | {
1545 | "text": "interested in wheels",
1546 | "intent": "Explore",
1547 | "entities": [
1548 | {
1549 | "entity": "Entity",
1550 | "startPos": 14,
1551 | "endPos": 19
1552 | }
1553 | ]
1554 | },
1555 | {
1556 | "text": "i need some tires and tubes",
1557 | "intent": "Explore",
1558 | "entities": [
1559 | {
1560 | "entity": "Entity",
1561 | "startPos": 12,
1562 | "endPos": 16
1563 | },
1564 | {
1565 | "entity": "Entity",
1566 | "startPos": 22,
1567 | "endPos": 26
1568 | }
1569 | ]
1570 | },
1571 | {
1572 | "text": "i need some bottles and cages",
1573 | "intent": "Explore",
1574 | "entities": [
1575 | {
1576 | "entity": "Entity",
1577 | "startPos": 12,
1578 | "endPos": 18
1579 | },
1580 | {
1581 | "entity": "Entity",
1582 | "startPos": 24,
1583 | "endPos": 28
1584 | }
1585 | ]
1586 | },
1587 | {
1588 | "text": "what kind of headsets do you have?",
1589 | "intent": "Explore",
1590 | "entities": [
1591 | {
1592 | "entity": "Entity",
1593 | "startPos": 13,
1594 | "endPos": 20
1595 | }
1596 | ]
1597 | },
1598 | {
1599 | "text": "show me some bib-shorts",
1600 | "intent": "Explore",
1601 | "entities": [
1602 | {
1603 | "entity": "Detail",
1604 | "startPos": 13,
1605 | "endPos": 15
1606 | },
1607 | {
1608 | "entity": "Entity",
1609 | "startPos": 17,
1610 | "endPos": 22
1611 | }
1612 | ]
1613 | },
1614 | {
1615 | "text": "what kind of helmets do you have?",
1616 | "intent": "Explore",
1617 | "entities": [
1618 | {
1619 | "entity": "Entity",
1620 | "startPos": 13,
1621 | "endPos": 19
1622 | }
1623 | ]
1624 | },
1625 | {
1626 | "text": "i would like to see fenders",
1627 | "intent": "Explore",
1628 | "entities": [
1629 | {
1630 | "entity": "Entity",
1631 | "startPos": 20,
1632 | "endPos": 26
1633 | }
1634 | ]
1635 | },
1636 | {
1637 | "text": "i need some wheels",
1638 | "intent": "Explore",
1639 | "entities": [
1640 | {
1641 | "entity": "Entity",
1642 | "startPos": 12,
1643 | "endPos": 17
1644 | }
1645 | ]
1646 | },
1647 | {
1648 | "text": "i would like to see handlebars",
1649 | "intent": "Explore",
1650 | "entities": [
1651 | {
1652 | "entity": "Entity",
1653 | "startPos": 20,
1654 | "endPos": 29
1655 | }
1656 | ]
1657 | },
1658 | {
1659 | "text": "i need some caps",
1660 | "intent": "Explore",
1661 | "entities": [
1662 | {
1663 | "entity": "Entity",
1664 | "startPos": 12,
1665 | "endPos": 15
1666 | }
1667 | ]
1668 | },
1669 | {
1670 | "text": "show me some bike stands",
1671 | "intent": "Explore",
1672 | "entities": [
1673 | {
1674 | "entity": "Entity",
1675 | "startPos": 18,
1676 | "endPos": 23
1677 | }
1678 | ]
1679 | },
1680 | {
1681 | "text": "what kind of mountain bikes do you have?",
1682 | "intent": "Explore",
1683 | "entities": [
1684 | {
1685 | "entity": "Detail",
1686 | "startPos": 13,
1687 | "endPos": 20
1688 | },
1689 | {
1690 | "entity": "Entity",
1691 | "startPos": 22,
1692 | "endPos": 26
1693 | }
1694 | ]
1695 | },
1696 | {
1697 | "text": "i need some headsets",
1698 | "intent": "Explore",
1699 | "entities": [
1700 | {
1701 | "entity": "Entity",
1702 | "startPos": 12,
1703 | "endPos": 19
1704 | }
1705 | ]
1706 | },
1707 | {
1708 | "text": "interested in bottom brackets",
1709 | "intent": "Explore",
1710 | "entities": [
1711 | {
1712 | "entity": "Entity",
1713 | "startPos": 21,
1714 | "endPos": 28
1715 | }
1716 | ]
1717 | },
1718 | {
1719 | "text": "i need some saddles",
1720 | "intent": "Explore",
1721 | "entities": [
1722 | {
1723 | "entity": "Entity",
1724 | "startPos": 12,
1725 | "endPos": 18
1726 | }
1727 | ]
1728 | },
1729 | {
1730 | "text": "what kind of touring bikes do you have?",
1731 | "intent": "Explore",
1732 | "entities": [
1733 | {
1734 | "entity": "Detail",
1735 | "startPos": 13,
1736 | "endPos": 19
1737 | },
1738 | {
1739 | "entity": "Entity",
1740 | "startPos": 21,
1741 | "endPos": 25
1742 | }
1743 | ]
1744 | },
1745 | {
1746 | "text": "please show me mountain frames",
1747 | "intent": "Explore",
1748 | "entities": [
1749 | {
1750 | "entity": "Detail",
1751 | "startPos": 15,
1752 | "endPos": 22
1753 | },
1754 | {
1755 | "entity": "Entity",
1756 | "startPos": 24,
1757 | "endPos": 29
1758 | }
1759 | ]
1760 | },
1761 | {
1762 | "text": "show me some bike racks",
1763 | "intent": "Explore",
1764 | "entities": [
1765 | {
1766 | "entity": "Detail",
1767 | "startPos": 13,
1768 | "endPos": 16
1769 | },
1770 | {
1771 | "entity": "Entity",
1772 | "startPos": 18,
1773 | "endPos": 22
1774 | }
1775 | ]
1776 | },
1777 | {
1778 | "text": "what kind of shorts do you have?",
1779 | "intent": "Explore",
1780 | "entities": [
1781 | {
1782 | "entity": "Entity",
1783 | "startPos": 13,
1784 | "endPos": 18
1785 | }
1786 | ]
1787 | },
1788 | {
1789 | "text": "please show me road bikes",
1790 | "intent": "Explore",
1791 | "entities": [
1792 | {
1793 | "entity": "Detail",
1794 | "startPos": 15,
1795 | "endPos": 18
1796 | },
1797 | {
1798 | "entity": "Entity",
1799 | "startPos": 20,
1800 | "endPos": 24
1801 | }
1802 | ]
1803 | },
1804 | {
1805 | "text": "i would like to see socks",
1806 | "intent": "Explore",
1807 | "entities": [
1808 | {
1809 | "entity": "Entity",
1810 | "startPos": 20,
1811 | "endPos": 24
1812 | }
1813 | ]
1814 | },
1815 | {
1816 | "text": "what kind of cleaners do you have?",
1817 | "intent": "Explore",
1818 | "entities": [
1819 | {
1820 | "entity": "Entity",
1821 | "startPos": 13,
1822 | "endPos": 20
1823 | }
1824 | ]
1825 | },
1826 | {
1827 | "text": "what kind of gloves do you have?",
1828 | "intent": "Explore",
1829 | "entities": [
1830 | {
1831 | "entity": "Entity",
1832 | "startPos": 13,
1833 | "endPos": 18
1834 | }
1835 | ]
1836 | },
1837 | {
1838 | "text": "what kind of saddles do you have?",
1839 | "intent": "Explore",
1840 | "entities": [
1841 | {
1842 | "entity": "Entity",
1843 | "startPos": 13,
1844 | "endPos": 19
1845 | }
1846 | ]
1847 | },
1848 | {
1849 | "text": "i need some bike stands",
1850 | "intent": "Explore",
1851 | "entities": [
1852 | {
1853 | "entity": "Detail",
1854 | "startPos": 12,
1855 | "endPos": 15
1856 | },
1857 | {
1858 | "entity": "Entity",
1859 | "startPos": 17,
1860 | "endPos": 22
1861 | }
1862 | ]
1863 | },
1864 | {
1865 | "text": "interested in lights",
1866 | "intent": "Explore",
1867 | "entities": [
1868 | {
1869 | "entity": "Entity",
1870 | "startPos": 14,
1871 | "endPos": 19
1872 | }
1873 | ]
1874 | },
1875 | {
1876 | "text": "show me some road frames",
1877 | "intent": "Explore",
1878 | "entities": [
1879 | {
1880 | "entity": "Entity",
1881 | "startPos": 18,
1882 | "endPos": 23
1883 | }
1884 | ]
1885 | },
1886 | {
1887 | "text": "i would like to see helmets",
1888 | "intent": "Explore",
1889 | "entities": [
1890 | {
1891 | "entity": "Entity",
1892 | "startPos": 20,
1893 | "endPos": 26
1894 | }
1895 | ]
1896 | },
1897 | {
1898 | "text": "interested in cleaners",
1899 | "intent": "Explore",
1900 | "entities": [
1901 | {
1902 | "entity": "Entity",
1903 | "startPos": 14,
1904 | "endPos": 21
1905 | }
1906 | ]
1907 | },
1908 | {
1909 | "text": "please show me touring frames",
1910 | "intent": "Explore",
1911 | "entities": [
1912 | {
1913 | "entity": "Detail",
1914 | "startPos": 15,
1915 | "endPos": 21
1916 | },
1917 | {
1918 | "entity": "Entity",
1919 | "startPos": 23,
1920 | "endPos": 28
1921 | }
1922 | ]
1923 | },
1924 | {
1925 | "text": "interested in gloves",
1926 | "intent": "Explore",
1927 | "entities": [
1928 | {
1929 | "entity": "Entity",
1930 | "startPos": 14,
1931 | "endPos": 19
1932 | }
1933 | ]
1934 | },
1935 | {
1936 | "text": "what kind of caps do you have?",
1937 | "intent": "Explore",
1938 | "entities": [
1939 | {
1940 | "entity": "Entity",
1941 | "startPos": 13,
1942 | "endPos": 16
1943 | }
1944 | ]
1945 | },
1946 | {
1947 | "text": "please show me cleaners",
1948 | "intent": "Explore",
1949 | "entities": [
1950 | {
1951 | "entity": "Entity",
1952 | "startPos": 15,
1953 | "endPos": 22
1954 | }
1955 | ]
1956 | },
1957 | {
1958 | "text": "interested in bike racks",
1959 | "intent": "Explore",
1960 | "entities": [
1961 | {
1962 | "entity": "Entity",
1963 | "startPos": 19,
1964 | "endPos": 23
1965 | }
1966 | ]
1967 | },
1968 | {
1969 | "text": "please show me helmets",
1970 | "intent": "Explore",
1971 | "entities": [
1972 | {
1973 | "entity": "Entity",
1974 | "startPos": 15,
1975 | "endPos": 21
1976 | }
1977 | ]
1978 | },
1979 | {
1980 | "text": "please show me mountain bikes",
1981 | "intent": "Explore",
1982 | "entities": [
1983 | {
1984 | "entity": "Detail",
1985 | "startPos": 15,
1986 | "endPos": 22
1987 | },
1988 | {
1989 | "entity": "Entity",
1990 | "startPos": 24,
1991 | "endPos": 28
1992 | }
1993 | ]
1994 | },
1995 | {
1996 | "text": "interested in road frames",
1997 | "intent": "Explore",
1998 | "entities": [
1999 | {
2000 | "entity": "Entity",
2001 | "startPos": 19,
2002 | "endPos": 24
2003 | }
2004 | ]
2005 | },
2006 | {
2007 | "text": "i would like to see shorts",
2008 | "intent": "Explore",
2009 | "entities": [
2010 | {
2011 | "entity": "Entity",
2012 | "startPos": 20,
2013 | "endPos": 25
2014 | }
2015 | ]
2016 | },
2017 | {
2018 | "text": "please show me touring bikes",
2019 | "intent": "Explore",
2020 | "entities": [
2021 | {
2022 | "entity": "Detail",
2023 | "startPos": 15,
2024 | "endPos": 21
2025 | },
2026 | {
2027 | "entity": "Entity",
2028 | "startPos": 23,
2029 | "endPos": 27
2030 | }
2031 | ]
2032 | },
2033 | {
2034 | "text": "interested in mountain frames",
2035 | "intent": "Explore",
2036 | "entities": [
2037 | {
2038 | "entity": "Entity",
2039 | "startPos": 23,
2040 | "endPos": 28
2041 | }
2042 | ]
2043 | },
2044 | {
2045 | "text": "please show me shorts",
2046 | "intent": "Explore",
2047 | "entities": [
2048 | {
2049 | "entity": "Entity",
2050 | "startPos": 15,
2051 | "endPos": 20
2052 | }
2053 | ]
2054 | },
2055 | {
2056 | "text": "show me some mountain frames",
2057 | "intent": "Explore",
2058 | "entities": [
2059 | {
2060 | "entity": "Detail",
2061 | "startPos": 13,
2062 | "endPos": 20
2063 | },
2064 | {
2065 | "entity": "Entity",
2066 | "startPos": 22,
2067 | "endPos": 27
2068 | }
2069 | ]
2070 | },
2071 | {
2072 | "text": "show me some road bikes",
2073 | "intent": "Explore",
2074 | "entities": [
2075 | {
2076 | "entity": "Detail",
2077 | "startPos": 13,
2078 | "endPos": 16
2079 | },
2080 | {
2081 | "entity": "Entity",
2082 | "startPos": 18,
2083 | "endPos": 22
2084 | }
2085 | ]
2086 | },
2087 | {
2088 | "text": "what kind of derailleurs do you have?",
2089 | "intent": "Explore",
2090 | "entities": [
2091 | {
2092 | "entity": "Entity",
2093 | "startPos": 13,
2094 | "endPos": 23
2095 | }
2096 | ]
2097 | },
2098 | {
2099 | "text": "what kind of panniers do you have?",
2100 | "intent": "Explore",
2101 | "entities": [
2102 | {
2103 | "entity": "Entity",
2104 | "startPos": 13,
2105 | "endPos": 20
2106 | }
2107 | ]
2108 | },
2109 | {
2110 | "text": "i need some road frames",
2111 | "intent": "Explore",
2112 | "entities": [
2113 | {
2114 | "entity": "Detail",
2115 | "startPos": 12,
2116 | "endPos": 15
2117 | },
2118 | {
2119 | "entity": "Entity",
2120 | "startPos": 17,
2121 | "endPos": 22
2122 | }
2123 | ]
2124 | },
2125 | {
2126 | "text": "what kind of cranksets do you have?",
2127 | "intent": "Explore",
2128 | "entities": [
2129 | {
2130 | "entity": "Entity",
2131 | "startPos": 13,
2132 | "endPos": 21
2133 | }
2134 | ]
2135 | },
2136 | {
2137 | "text": "show me some mountain bikes",
2138 | "intent": "Explore",
2139 | "entities": [
2140 | {
2141 | "entity": "Detail",
2142 | "startPos": 13,
2143 | "endPos": 20
2144 | },
2145 | {
2146 | "entity": "Entity",
2147 | "startPos": 22,
2148 | "endPos": 26
2149 | }
2150 | ]
2151 | },
2152 | {
2153 | "text": "show me some touring frames",
2154 | "intent": "Explore",
2155 | "entities": [
2156 | {
2157 | "entity": "Detail",
2158 | "startPos": 13,
2159 | "endPos": 19
2160 | },
2161 | {
2162 | "entity": "Entity",
2163 | "startPos": 21,
2164 | "endPos": 26
2165 | }
2166 | ]
2167 | },
2168 | {
2169 | "text": "interested in road bikes",
2170 | "intent": "Explore",
2171 | "entities": [
2172 | {
2173 | "entity": "Entity",
2174 | "startPos": 19,
2175 | "endPos": 23
2176 | }
2177 | ]
2178 | },
2179 | {
2180 | "text": "i need some mountain frames",
2181 | "intent": "Explore",
2182 | "entities": [
2183 | {
2184 | "entity": "Detail",
2185 | "startPos": 12,
2186 | "endPos": 19
2187 | },
2188 | {
2189 | "entity": "Entity",
2190 | "startPos": 21,
2191 | "endPos": 26
2192 | }
2193 | ]
2194 | },
2195 | {
2196 | "text": "i would like to see derailleurs",
2197 | "intent": "Explore",
2198 | "entities": [
2199 | {
2200 | "entity": "Entity",
2201 | "startPos": 20,
2202 | "endPos": 30
2203 | }
2204 | ]
2205 | },
2206 | {
2207 | "text": "interested in touring frames",
2208 | "intent": "Explore",
2209 | "entities": [
2210 | {
2211 | "entity": "Entity",
2212 | "startPos": 22,
2213 | "endPos": 27
2214 | }
2215 | ]
2216 | },
2217 | {
2218 | "text": "i need some road bikes",
2219 | "intent": "Explore",
2220 | "entities": [
2221 | {
2222 | "entity": "Detail",
2223 | "startPos": 12,
2224 | "endPos": 15
2225 | },
2226 | {
2227 | "entity": "Entity",
2228 | "startPos": 17,
2229 | "endPos": 21
2230 | }
2231 | ]
2232 | },
2233 | {
2234 | "text": "i would like to see cranksets",
2235 | "intent": "Explore",
2236 | "entities": [
2237 | {
2238 | "entity": "Entity",
2239 | "startPos": 20,
2240 | "endPos": 28
2241 | }
2242 | ]
2243 | },
2244 | {
2245 | "text": "i need some shorts",
2246 | "intent": "Explore",
2247 | "entities": [
2248 | {
2249 | "entity": "Entity",
2250 | "startPos": 12,
2251 | "endPos": 17
2252 | }
2253 | ]
2254 | },
2255 | {
2256 | "text": "interested in mountain bikes",
2257 | "intent": "Explore",
2258 | "entities": [
2259 | {
2260 | "entity": "Detail",
2261 | "startPos": 14,
2262 | "endPos": 21
2263 | },
2264 | {
2265 | "entity": "Entity",
2266 | "startPos": 23,
2267 | "endPos": 27
2268 | }
2269 | ]
2270 | },
2271 | {
2272 | "text": "i would like to see panniers",
2273 | "intent": "Explore",
2274 | "entities": [
2275 | {
2276 | "entity": "Entity",
2277 | "startPos": 20,
2278 | "endPos": 27
2279 | }
2280 | ]
2281 | },
2282 | {
2283 | "text": "show me some touring bikes",
2284 | "intent": "Explore",
2285 | "entities": [
2286 | {
2287 | "entity": "Detail",
2288 | "startPos": 13,
2289 | "endPos": 19
2290 | },
2291 | {
2292 | "entity": "Entity",
2293 | "startPos": 21,
2294 | "endPos": 25
2295 | }
2296 | ]
2297 | },
2298 | {
2299 | "text": "i need some touring frames",
2300 | "intent": "Explore",
2301 | "entities": [
2302 | {
2303 | "entity": "Detail",
2304 | "startPos": 12,
2305 | "endPos": 18
2306 | },
2307 | {
2308 | "entity": "Entity",
2309 | "startPos": 20,
2310 | "endPos": 25
2311 | }
2312 | ]
2313 | },
2314 | {
2315 | "text": "interested in touring bikes",
2316 | "intent": "Explore",
2317 | "entities": [
2318 | {
2319 | "entity": "Detail",
2320 | "startPos": 14,
2321 | "endPos": 20
2322 | },
2323 | {
2324 | "entity": "Entity",
2325 | "startPos": 22,
2326 | "endPos": 26
2327 | }
2328 | ]
2329 | },
2330 | {
2331 | "text": "i need some cranksets",
2332 | "intent": "Explore",
2333 | "entities": [
2334 | {
2335 | "entity": "Entity",
2336 | "startPos": 12,
2337 | "endPos": 20
2338 | }
2339 | ]
2340 | },
2341 | {
2342 | "text": "please show me fenders",
2343 | "intent": "Explore",
2344 | "entities": [
2345 | {
2346 | "entity": "Entity",
2347 | "startPos": 15,
2348 | "endPos": 21
2349 | }
2350 | ]
2351 | },
2352 | {
2353 | "text": "interested in forks",
2354 | "intent": "Explore",
2355 | "entities": [
2356 | {
2357 | "entity": "Entity",
2358 | "startPos": 14,
2359 | "endPos": 18
2360 | }
2361 | ]
2362 | },
2363 | {
2364 | "text": "interested in pedals",
2365 | "intent": "Explore",
2366 | "entities": [
2367 | {
2368 | "entity": "Entity",
2369 | "startPos": 14,
2370 | "endPos": 19
2371 | }
2372 | ]
2373 | },
2374 | {
2375 | "text": "interested in chains",
2376 | "intent": "Explore",
2377 | "entities": [
2378 | {
2379 | "entity": "Entity",
2380 | "startPos": 14,
2381 | "endPos": 19
2382 | }
2383 | ]
2384 | },
2385 | {
2386 | "text": "please show me vests",
2387 | "intent": "Explore",
2388 | "entities": [
2389 | {
2390 | "entity": "Entity",
2391 | "startPos": 15,
2392 | "endPos": 19
2393 | }
2394 | ]
2395 | },
2396 | {
2397 | "text": "show me some bottles and cages",
2398 | "intent": "Explore",
2399 | "entities": [
2400 | {
2401 | "entity": "Entity",
2402 | "startPos": 13,
2403 | "endPos": 19
2404 | },
2405 | {
2406 | "entity": "Entity",
2407 | "startPos": 25,
2408 | "endPos": 29
2409 | }
2410 | ]
2411 | },
2412 | {
2413 | "text": "please show me cranksets",
2414 | "intent": "Explore",
2415 | "entities": [
2416 | {
2417 | "entity": "Entity",
2418 | "startPos": 15,
2419 | "endPos": 23
2420 | }
2421 | ]
2422 | },
2423 | {
2424 | "text": "i need some vests",
2425 | "intent": "Explore",
2426 | "entities": [
2427 | {
2428 | "entity": "Entity",
2429 | "startPos": 12,
2430 | "endPos": 16
2431 | }
2432 | ]
2433 | },
2434 | {
2435 | "text": "show me some pedals",
2436 | "intent": "Explore",
2437 | "entities": [
2438 | {
2439 | "entity": "Entity",
2440 | "startPos": 13,
2441 | "endPos": 18
2442 | }
2443 | ]
2444 | },
2445 | {
2446 | "text": "show me some socks",
2447 | "intent": "Explore",
2448 | "entities": [
2449 | {
2450 | "entity": "Entity",
2451 | "startPos": 13,
2452 | "endPos": 17
2453 | }
2454 | ]
2455 | },
2456 | {
2457 | "text": "i need some panniers",
2458 | "intent": "Explore",
2459 | "entities": [
2460 | {
2461 | "entity": "Entity",
2462 | "startPos": 12,
2463 | "endPos": 19
2464 | }
2465 | ]
2466 | },
2467 | {
2468 | "text": "i need some derailleurs",
2469 | "intent": "Explore",
2470 | "entities": [
2471 | {
2472 | "entity": "Entity",
2473 | "startPos": 12,
2474 | "endPos": 22
2475 | }
2476 | ]
2477 | },
2478 | {
2479 | "text": "please show me panniers",
2480 | "intent": "Explore",
2481 | "entities": [
2482 | {
2483 | "entity": "Entity",
2484 | "startPos": 15,
2485 | "endPos": 22
2486 | }
2487 | ]
2488 | },
2489 | {
2490 | "text": "interested in shorts",
2491 | "intent": "Explore",
2492 | "entities": [
2493 | {
2494 | "entity": "Entity",
2495 | "startPos": 14,
2496 | "endPos": 19
2497 | }
2498 | ]
2499 | },
2500 | {
2501 | "text": "please show me saddles",
2502 | "intent": "Explore",
2503 | "entities": [
2504 | {
2505 | "entity": "Entity",
2506 | "startPos": 15,
2507 | "endPos": 21
2508 | }
2509 | ]
2510 | },
2511 | {
2512 | "text": "please show me wheels",
2513 | "intent": "Explore",
2514 | "entities": [
2515 | {
2516 | "entity": "Entity",
2517 | "startPos": 15,
2518 | "endPos": 20
2519 | }
2520 | ]
2521 | },
2522 | {
2523 | "text": "show me some shorts",
2524 | "intent": "Explore",
2525 | "entities": [
2526 | {
2527 | "entity": "Entity",
2528 | "startPos": 13,
2529 | "endPos": 18
2530 | }
2531 | ]
2532 | },
2533 | {
2534 | "text": "i need some helmets",
2535 | "intent": "Explore",
2536 | "entities": [
2537 | {
2538 | "entity": "Entity",
2539 | "startPos": 12,
2540 | "endPos": 18
2541 | }
2542 | ]
2543 | },
2544 | {
2545 | "text": "i need some pedals",
2546 | "intent": "Explore",
2547 | "entities": [
2548 | {
2549 | "entity": "Entity",
2550 | "startPos": 12,
2551 | "endPos": 17
2552 | }
2553 | ]
2554 | },
2555 | {
2556 | "text": "please show me tights",
2557 | "intent": "Explore",
2558 | "entities": [
2559 | {
2560 | "entity": "Entity",
2561 | "startPos": 15,
2562 | "endPos": 20
2563 | }
2564 | ]
2565 | },
2566 | {
2567 | "text": "show me some fenders",
2568 | "intent": "Explore",
2569 | "entities": [
2570 | {
2571 | "entity": "Entity",
2572 | "startPos": 13,
2573 | "endPos": 19
2574 | }
2575 | ]
2576 | },
2577 | {
2578 | "text": "show me some tights",
2579 | "intent": "Explore",
2580 | "entities": [
2581 | {
2582 | "entity": "Entity",
2583 | "startPos": 13,
2584 | "endPos": 18
2585 | }
2586 | ]
2587 | },
2588 | {
2589 | "text": "what kind of pumps do you have?",
2590 | "intent": "Explore",
2591 | "entities": [
2592 | {
2593 | "entity": "Entity",
2594 | "startPos": 13,
2595 | "endPos": 17
2596 | }
2597 | ]
2598 | },
2599 | {
2600 | "text": "what kind of jerseys do you have?",
2601 | "intent": "Explore",
2602 | "entities": [
2603 | {
2604 | "entity": "Entity",
2605 | "startPos": 13,
2606 | "endPos": 19
2607 | }
2608 | ]
2609 | },
2610 | {
2611 | "text": "show me some locks",
2612 | "intent": "Explore",
2613 | "entities": [
2614 | {
2615 | "entity": "Entity",
2616 | "startPos": 13,
2617 | "endPos": 17
2618 | }
2619 | ]
2620 | },
2621 | {
2622 | "text": "please show me pumps",
2623 | "intent": "Explore",
2624 | "entities": [
2625 | {
2626 | "entity": "Entity",
2627 | "startPos": 15,
2628 | "endPos": 19
2629 | }
2630 | ]
2631 | },
2632 | {
2633 | "text": "show me some lights",
2634 | "intent": "Explore",
2635 | "entities": [
2636 | {
2637 | "entity": "Entity",
2638 | "startPos": 13,
2639 | "endPos": 18
2640 | }
2641 | ]
2642 | },
2643 | {
2644 | "text": "what kind of bottles and cages do you have?",
2645 | "intent": "Explore",
2646 | "entities": [
2647 | {
2648 | "entity": "Entity",
2649 | "startPos": 13,
2650 | "endPos": 19
2651 | },
2652 | {
2653 | "entity": "Entity",
2654 | "startPos": 25,
2655 | "endPos": 29
2656 | }
2657 | ]
2658 | },
2659 | {
2660 | "text": "please show me lights",
2661 | "intent": "Explore",
2662 | "entities": [
2663 | {
2664 | "entity": "Entity",
2665 | "startPos": 15,
2666 | "endPos": 20
2667 | }
2668 | ]
2669 | },
2670 | {
2671 | "text": "interested in locks",
2672 | "intent": "Explore",
2673 | "entities": [
2674 | {
2675 | "entity": "Entity",
2676 | "startPos": 14,
2677 | "endPos": 18
2678 | }
2679 | ]
2680 | },
2681 | {
2682 | "text": "interested in derailleurs",
2683 | "intent": "Explore",
2684 | "entities": [
2685 | {
2686 | "entity": "Entity",
2687 | "startPos": 14,
2688 | "endPos": 24
2689 | }
2690 | ]
2691 | },
2692 | {
2693 | "text": "interested in panniers",
2694 | "intent": "Explore",
2695 | "entities": [
2696 | {
2697 | "entity": "Entity",
2698 | "startPos": 14,
2699 | "endPos": 21
2700 | }
2701 | ]
2702 | },
2703 | {
2704 | "text": "show me some cranksets",
2705 | "intent": "Explore",
2706 | "entities": [
2707 | {
2708 | "entity": "Entity",
2709 | "startPos": 13,
2710 | "endPos": 21
2711 | }
2712 | ]
2713 | },
2714 | {
2715 | "text": "what kind of tires and tubes do you have?",
2716 | "intent": "Explore",
2717 | "entities": [
2718 | {
2719 | "entity": "Entity",
2720 | "startPos": 13,
2721 | "endPos": 17
2722 | },
2723 | {
2724 | "entity": "Entity",
2725 | "startPos": 23,
2726 | "endPos": 27
2727 | }
2728 | ]
2729 | },
2730 | {
2731 | "text": "i need some fenders",
2732 | "intent": "Explore",
2733 | "entities": [
2734 | {
2735 | "entity": "Entity",
2736 | "startPos": 12,
2737 | "endPos": 18
2738 | }
2739 | ]
2740 | },
2741 | {
2742 | "text": "what kind of tights do you have?",
2743 | "intent": "Explore",
2744 | "entities": [
2745 | {
2746 | "entity": "Entity",
2747 | "startPos": 13,
2748 | "endPos": 18
2749 | }
2750 | ]
2751 | },
2752 | {
2753 | "text": "please show me headsets",
2754 | "intent": "Explore",
2755 | "entities": [
2756 | {
2757 | "entity": "Entity",
2758 | "startPos": 15,
2759 | "endPos": 22
2760 | }
2761 | ]
2762 | },
2763 | {
2764 | "text": "please show me caps",
2765 | "intent": "Explore",
2766 | "entities": [
2767 | {
2768 | "entity": "Entity",
2769 | "startPos": 15,
2770 | "endPos": 18
2771 | }
2772 | ]
2773 | },
2774 | {
2775 | "text": "please show me jerseys",
2776 | "intent": "Explore",
2777 | "entities": [
2778 | {
2779 | "entity": "Entity",
2780 | "startPos": 15,
2781 | "endPos": 21
2782 | }
2783 | ]
2784 | },
2785 | {
2786 | "text": "show me some saddles",
2787 | "intent": "Explore",
2788 | "entities": [
2789 | {
2790 | "entity": "Entity",
2791 | "startPos": 13,
2792 | "endPos": 19
2793 | }
2794 | ]
2795 | },
2796 | {
2797 | "text": "show me some forks",
2798 | "intent": "Explore",
2799 | "entities": [
2800 | {
2801 | "entity": "Entity",
2802 | "startPos": 13,
2803 | "endPos": 17
2804 | }
2805 | ]
2806 | },
2807 | {
2808 | "text": "show me some cleaners",
2809 | "intent": "Explore",
2810 | "entities": [
2811 | {
2812 | "entity": "Entity",
2813 | "startPos": 13,
2814 | "endPos": 20
2815 | }
2816 | ]
2817 | },
2818 | {
2819 | "text": "interested in helmets",
2820 | "intent": "Explore",
2821 | "entities": [
2822 | {
2823 | "entity": "Entity",
2824 | "startPos": 14,
2825 | "endPos": 20
2826 | }
2827 | ]
2828 | },
2829 | {
2830 | "text": "please show me handlebars",
2831 | "intent": "Explore",
2832 | "entities": [
2833 | {
2834 | "entity": "Entity",
2835 | "startPos": 15,
2836 | "endPos": 24
2837 | }
2838 | ]
2839 | },
2840 | {
2841 | "text": "show me some vests",
2842 | "intent": "Explore",
2843 | "entities": [
2844 | {
2845 | "entity": "Entity",
2846 | "startPos": 13,
2847 | "endPos": 17
2848 | }
2849 | ]
2850 | },
2851 | {
2852 | "text": "interested in brakes",
2853 | "intent": "Explore",
2854 | "entities": [
2855 | {
2856 | "entity": "Entity",
2857 | "startPos": 14,
2858 | "endPos": 19
2859 | }
2860 | ]
2861 | },
2862 | {
2863 | "text": "show me some pumps",
2864 | "intent": "Explore",
2865 | "entities": [
2866 | {
2867 | "entity": "Entity",
2868 | "startPos": 13,
2869 | "endPos": 17
2870 | }
2871 | ]
2872 | },
2873 | {
2874 | "text": "please show me tires and tubes",
2875 | "intent": "Explore",
2876 | "entities": [
2877 | {
2878 | "entity": "Entity",
2879 | "startPos": 15,
2880 | "endPos": 19
2881 | },
2882 | {
2883 | "entity": "Entity",
2884 | "startPos": 25,
2885 | "endPos": 29
2886 | }
2887 | ]
2888 | },
2889 | {
2890 | "text": "show me some headsets",
2891 | "intent": "Explore",
2892 | "entities": [
2893 | {
2894 | "entity": "Entity",
2895 | "startPos": 13,
2896 | "endPos": 20
2897 | }
2898 | ]
2899 | },
2900 | {
2901 | "text": "show me some tires and tubes",
2902 | "intent": "Explore",
2903 | "entities": [
2904 | {
2905 | "entity": "Entity",
2906 | "startPos": 13,
2907 | "endPos": 17
2908 | },
2909 | {
2910 | "entity": "Entity",
2911 | "startPos": 23,
2912 | "endPos": 27
2913 | }
2914 | ]
2915 | },
2916 | {
2917 | "text": "show me some handlebars",
2918 | "intent": "Explore",
2919 | "entities": [
2920 | {
2921 | "entity": "Entity",
2922 | "startPos": 13,
2923 | "endPos": 22
2924 | }
2925 | ]
2926 | },
2927 | {
2928 | "text": "show me some wheels",
2929 | "intent": "Explore",
2930 | "entities": [
2931 | {
2932 | "entity": "Entity",
2933 | "startPos": 13,
2934 | "endPos": 18
2935 | }
2936 | ]
2937 | },
2938 | {
2939 | "text": "interested in handlebars",
2940 | "intent": "Explore",
2941 | "entities": [
2942 | {
2943 | "entity": "Entity",
2944 | "startPos": 14,
2945 | "endPos": 23
2946 | }
2947 | ]
2948 | },
2949 | {
2950 | "text": "please show me derailleurs",
2951 | "intent": "Explore",
2952 | "entities": [
2953 | {
2954 | "entity": "Entity",
2955 | "startPos": 15,
2956 | "endPos": 25
2957 | }
2958 | ]
2959 | },
2960 | {
2961 | "text": "show me some helmets",
2962 | "intent": "Explore",
2963 | "entities": [
2964 | {
2965 | "entity": "Entity",
2966 | "startPos": 13,
2967 | "endPos": 19
2968 | }
2969 | ]
2970 | },
2971 | {
2972 | "text": "show me some brakes",
2973 | "intent": "Explore",
2974 | "entities": [
2975 | {
2976 | "entity": "Entity",
2977 | "startPos": 13,
2978 | "endPos": 18
2979 | }
2980 | ]
2981 | },
2982 | {
2983 | "text": "show me some chains",
2984 | "intent": "Explore",
2985 | "entities": [
2986 | {
2987 | "entity": "Entity",
2988 | "startPos": 13,
2989 | "endPos": 18
2990 | }
2991 | ]
2992 | },
2993 | {
2994 | "text": "show me some jerseys",
2995 | "intent": "Explore",
2996 | "entities": [
2997 | {
2998 | "entity": "Entity",
2999 | "startPos": 13,
3000 | "endPos": 19
3001 | }
3002 | ]
3003 | },
3004 | {
3005 | "text": "interested in bottles and cages",
3006 | "intent": "Explore",
3007 | "entities": [
3008 | {
3009 | "entity": "Entity",
3010 | "startPos": 14,
3011 | "endPos": 20
3012 | },
3013 | {
3014 | "entity": "Entity",
3015 | "startPos": 26,
3016 | "endPos": 30
3017 | }
3018 | ]
3019 | },
3020 | {
3021 | "text": "show me some panniers",
3022 | "intent": "Explore",
3023 | "entities": [
3024 | {
3025 | "entity": "Entity",
3026 | "startPos": 13,
3027 | "endPos": 20
3028 | }
3029 | ]
3030 | },
3031 | {
3032 | "text": "interested in cranksets",
3033 | "intent": "Explore",
3034 | "entities": [
3035 | {
3036 | "entity": "Entity",
3037 | "startPos": 14,
3038 | "endPos": 22
3039 | }
3040 | ]
3041 | },
3042 | {
3043 | "text": "interested in socks",
3044 | "intent": "Explore",
3045 | "entities": [
3046 | {
3047 | "entity": "Entity",
3048 | "startPos": 14,
3049 | "endPos": 18
3050 | }
3051 | ]
3052 | },
3053 | {
3054 | "text": "show me some caps",
3055 | "intent": "Explore",
3056 | "entities": [
3057 | {
3058 | "entity": "Entity",
3059 | "startPos": 13,
3060 | "endPos": 16
3061 | }
3062 | ]
3063 | },
3064 | {
3065 | "text": "what about bikes?",
3066 | "intent": "Explore",
3067 | "entities": [
3068 | {
3069 | "entity": "Entity",
3070 | "startPos": 11,
3071 | "endPos": 15
3072 | }
3073 | ]
3074 | },
3075 | {
3076 | "text": "maybe you have frames?",
3077 | "intent": "Explore",
3078 | "entities": [
3079 | {
3080 | "entity": "Entity",
3081 | "startPos": 15,
3082 | "endPos": 20
3083 | }
3084 | ]
3085 | },
3086 | {
3087 | "text": "maybe you've got wheels?",
3088 | "intent": "Explore",
3089 | "entities": [
3090 | {
3091 | "entity": "Entity",
3092 | "startPos": 17,
3093 | "endPos": 22
3094 | }
3095 | ]
3096 | },
3097 | {
3098 | "text": "do you have bikes?",
3099 | "intent": "Explore",
3100 | "entities": [
3101 | {
3102 | "entity": "Entity",
3103 | "startPos": 12,
3104 | "endPos": 16
3105 | }
3106 | ]
3107 | },
3108 | {
3109 | "text": "what touring bikes?",
3110 | "intent": "Explore",
3111 | "entities": [
3112 | {
3113 | "entity": "Detail",
3114 | "startPos": 5,
3115 | "endPos": 11
3116 | },
3117 | {
3118 | "entity": "Entity",
3119 | "startPos": 13,
3120 | "endPos": 17
3121 | }
3122 | ]
3123 | },
3124 | {
3125 | "text": "what touring bikes do you have",
3126 | "intent": "Explore",
3127 | "entities": [
3128 | {
3129 | "entity": "Detail",
3130 | "startPos": 5,
3131 | "endPos": 11
3132 | },
3133 | {
3134 | "entity": "Entity",
3135 | "startPos": 13,
3136 | "endPos": 17
3137 | }
3138 | ]
3139 | },
3140 | {
3141 | "text": "alright. what kind of bikes do you have?",
3142 | "intent": "Explore",
3143 | "entities": [
3144 | {
3145 | "entity": "Entity",
3146 | "startPos": 22,
3147 | "endPos": 26
3148 | }
3149 | ]
3150 | },
3151 | {
3152 | "text": "do you sell bikes?",
3153 | "intent": "Explore",
3154 | "entities": [
3155 | {
3156 | "entity": "Entity",
3157 | "startPos": 12,
3158 | "endPos": 16
3159 | }
3160 | ]
3161 | },
3162 | {
3163 | "text": "can you show me road bikes please?",
3164 | "intent": "Explore",
3165 | "entities": [
3166 | {
3167 | "entity": "Detail",
3168 | "startPos": 16,
3169 | "endPos": 19
3170 | },
3171 | {
3172 | "entity": "Entity",
3173 | "startPos": 21,
3174 | "endPos": 25
3175 | }
3176 | ]
3177 | },
3178 | {
3179 | "text": "do you have touring bikes/",
3180 | "intent": "Explore",
3181 | "entities": [
3182 | {
3183 | "entity": "Detail",
3184 | "startPos": 12,
3185 | "endPos": 18
3186 | },
3187 | {
3188 | "entity": "Entity",
3189 | "startPos": 20,
3190 | "endPos": 24
3191 | }
3192 | ]
3193 | },
3194 | {
3195 | "text": "what now. you don’t talk to me?",
3196 | "intent": "None",
3197 | "entities": []
3198 | },
3199 | {
3200 | "text": "rear wheels please",
3201 | "intent": "Explore",
3202 | "entities": [
3203 | {
3204 | "entity": "Detail",
3205 | "startPos": 0,
3206 | "endPos": 3
3207 | },
3208 | {
3209 | "entity": "Entity",
3210 | "startPos": 5,
3211 | "endPos": 10
3212 | }
3213 | ]
3214 | },
3215 | {
3216 | "text": "what’s in store",
3217 | "intent": "ShowTopCategories",
3218 | "entities": []
3219 | },
3220 | {
3221 | "text": "what do you sell again?",
3222 | "intent": "ShowTopCategories",
3223 | "entities": []
3224 | },
3225 | {
3226 | "text": "do you have bikes>",
3227 | "intent": "Explore",
3228 | "entities": [
3229 | {
3230 | "entity": "Entity",
3231 | "startPos": 12,
3232 | "endPos": 16
3233 | }
3234 | ]
3235 | },
3236 | {
3237 | "text": "so what do you sell again?",
3238 | "intent": "ShowTopCategories",
3239 | "entities": []
3240 | },
3241 | {
3242 | "text": "do you have bikes/",
3243 | "intent": "Explore",
3244 | "entities": [
3245 | {
3246 | "entity": "Entity",
3247 | "startPos": 12,
3248 | "endPos": 16
3249 | }
3250 | ]
3251 | },
3252 | {
3253 | "text": "can you show me touring bikes?",
3254 | "intent": "Explore",
3255 | "entities": [
3256 | {
3257 | "entity": "Detail",
3258 | "startPos": 16,
3259 | "endPos": 22
3260 | },
3261 | {
3262 | "entity": "Entity",
3263 | "startPos": 24,
3264 | "endPos": 28
3265 | }
3266 | ]
3267 | },
3268 | {
3269 | "text": "can you show me touring bikes/",
3270 | "intent": "Explore",
3271 | "entities": [
3272 | {
3273 | "entity": "Detail",
3274 | "startPos": 16,
3275 | "endPos": 22
3276 | },
3277 | {
3278 | "entity": "Entity",
3279 | "startPos": 24,
3280 | "endPos": 28
3281 | }
3282 | ]
3283 | },
3284 | {
3285 | "text": "touring bikes please",
3286 | "intent": "Explore",
3287 | "entities": [
3288 | {
3289 | "entity": "Detail",
3290 | "startPos": 0,
3291 | "endPos": 6
3292 | },
3293 | {
3294 | "entity": "Entity",
3295 | "startPos": 8,
3296 | "endPos": 12
3297 | }
3298 | ]
3299 | },
3300 | {
3301 | "text": "what kind of clothing do you have?",
3302 | "intent": "Explore",
3303 | "entities": [
3304 | {
3305 | "entity": "Entity",
3306 | "startPos": 13,
3307 | "endPos": 20
3308 | }
3309 | ]
3310 | },
3311 | {
3312 | "text": "can you show me shorts please?",
3313 | "intent": "Explore",
3314 | "entities": [
3315 | {
3316 | "entity": "Entity",
3317 | "startPos": 16,
3318 | "endPos": 21
3319 | }
3320 | ]
3321 | },
3322 | {
3323 | "text": "do you have accessories?",
3324 | "intent": "Explore",
3325 | "entities": [
3326 | {
3327 | "entity": "Entity",
3328 | "startPos": 12,
3329 | "endPos": 22
3330 | }
3331 | ]
3332 | },
3333 | {
3334 | "text": "can you show me road bikes again?",
3335 | "intent": "Explore",
3336 | "entities": [
3337 | {
3338 | "entity": "Detail",
3339 | "startPos": 16,
3340 | "endPos": 19
3341 | },
3342 | {
3343 | "entity": "Entity",
3344 | "startPos": 21,
3345 | "endPos": 25
3346 | }
3347 | ]
3348 | },
3349 | {
3350 | "text": "cleaners",
3351 | "intent": "Explore",
3352 | "entities": [
3353 | {
3354 | "entity": "Entity",
3355 | "startPos": 0,
3356 | "endPos": 7
3357 | }
3358 | ]
3359 | },
3360 | {
3361 | "text": "show me touring bikes",
3362 | "intent": "Explore",
3363 | "entities": [
3364 | {
3365 | "entity": "Detail",
3366 | "startPos": 8,
3367 | "endPos": 14
3368 | },
3369 | {
3370 | "entity": "Entity",
3371 | "startPos": 16,
3372 | "endPos": 20
3373 | }
3374 | ]
3375 | },
3376 | {
3377 | "text": "show me accessories",
3378 | "intent": "Explore",
3379 | "entities": [
3380 | {
3381 | "entity": "Entity",
3382 | "startPos": 8,
3383 | "endPos": 18
3384 | }
3385 | ]
3386 | },
3387 | {
3388 | "text": "do you have accessories",
3389 | "intent": "Explore",
3390 | "entities": [
3391 | {
3392 | "entity": "Entity",
3393 | "startPos": 12,
3394 | "endPos": 22
3395 | }
3396 | ]
3397 | },
3398 | {
3399 | "text": "show me clothing",
3400 | "intent": "Explore",
3401 | "entities": [
3402 | {
3403 | "entity": "Entity",
3404 | "startPos": 8,
3405 | "endPos": 15
3406 | }
3407 | ]
3408 | },
3409 | {
3410 | "text": "show me touring bikes again",
3411 | "intent": "Explore",
3412 | "entities": [
3413 | {
3414 | "entity": "Detail",
3415 | "startPos": 8,
3416 | "endPos": 14
3417 | },
3418 | {
3419 | "entity": "Entity",
3420 | "startPos": 16,
3421 | "endPos": 20
3422 | }
3423 | ]
3424 | },
3425 | {
3426 | "text": "i am interested in mountain bikes, did you have some?",
3427 | "intent": "Explore",
3428 | "entities": [
3429 | {
3430 | "entity": "Detail",
3431 | "startPos": 19,
3432 | "endPos": 26
3433 | },
3434 | {
3435 | "entity": "Entity",
3436 | "startPos": 28,
3437 | "endPos": 32
3438 | }
3439 | ]
3440 | },
3441 | {
3442 | "text": "what kind of road bikes?",
3443 | "intent": "Explore",
3444 | "entities": [
3445 | {
3446 | "entity": "Detail",
3447 | "startPos": 13,
3448 | "endPos": 16
3449 | },
3450 | {
3451 | "entity": "Entity",
3452 | "startPos": 18,
3453 | "endPos": 22
3454 | }
3455 | ]
3456 | },
3457 | {
3458 | "text": "show me touring bikes that you have",
3459 | "intent": "Explore",
3460 | "entities": [
3461 | {
3462 | "entity": "Detail",
3463 | "startPos": 8,
3464 | "endPos": 14
3465 | },
3466 | {
3467 | "entity": "Entity",
3468 | "startPos": 16,
3469 | "endPos": 20
3470 | }
3471 | ]
3472 | },
3473 | {
3474 | "text": "what kind of clothing?",
3475 | "intent": "Explore",
3476 | "entities": [
3477 | {
3478 | "entity": "Entity",
3479 | "startPos": 13,
3480 | "endPos": 20
3481 | }
3482 | ]
3483 | },
3484 | {
3485 | "text": "show me your touring bikes pelase",
3486 | "intent": "Explore",
3487 | "entities": [
3488 | {
3489 | "entity": "Detail",
3490 | "startPos": 13,
3491 | "endPos": 19
3492 | },
3493 | {
3494 | "entity": "Entity",
3495 | "startPos": 21,
3496 | "endPos": 25
3497 | }
3498 | ]
3499 | },
3500 | {
3501 | "text": "can you show me touring bikes again?",
3502 | "intent": "Explore",
3503 | "entities": [
3504 | {
3505 | "entity": "Detail",
3506 | "startPos": 16,
3507 | "endPos": 22
3508 | },
3509 | {
3510 | "entity": "Entity",
3511 | "startPos": 24,
3512 | "endPos": 28
3513 | }
3514 | ]
3515 | },
3516 | {
3517 | "text": "show me road bikes you have",
3518 | "intent": "Explore",
3519 | "entities": [
3520 | {
3521 | "entity": "Detail",
3522 | "startPos": 8,
3523 | "endPos": 11
3524 | },
3525 | {
3526 | "entity": "Entity",
3527 | "startPos": 13,
3528 | "endPos": 17
3529 | }
3530 | ]
3531 | },
3532 | {
3533 | "text": "cleaners please",
3534 | "intent": "Explore",
3535 | "entities": [
3536 | {
3537 | "entity": "Entity",
3538 | "startPos": 0,
3539 | "endPos": 7
3540 | }
3541 | ]
3542 | },
3543 | {
3544 | "text": "can you show me bikes plesae",
3545 | "intent": "Explore",
3546 | "entities": [
3547 | {
3548 | "entity": "Entity",
3549 | "startPos": 16,
3550 | "endPos": 20
3551 | }
3552 | ]
3553 | },
3554 | {
3555 | "text": "mountain bikes please",
3556 | "intent": "Explore",
3557 | "entities": [
3558 | {
3559 | "entity": "Detail",
3560 | "startPos": 0,
3561 | "endPos": 7
3562 | },
3563 | {
3564 | "entity": "Entity",
3565 | "startPos": 9,
3566 | "endPos": 13
3567 | }
3568 | ]
3569 | },
3570 | {
3571 | "text": "show me road bikes that you have",
3572 | "intent": "Explore",
3573 | "entities": [
3574 | {
3575 | "entity": "Detail",
3576 | "startPos": 8,
3577 | "endPos": 11
3578 | },
3579 | {
3580 | "entity": "Entity",
3581 | "startPos": 13,
3582 | "endPos": 17
3583 | }
3584 | ]
3585 | },
3586 | {
3587 | "text": "show me touring bikes plesae",
3588 | "intent": "Explore",
3589 | "entities": [
3590 | {
3591 | "entity": "Detail",
3592 | "startPos": 8,
3593 | "endPos": 14
3594 | },
3595 | {
3596 | "entity": "Entity",
3597 | "startPos": 16,
3598 | "endPos": 20
3599 | }
3600 | ]
3601 | },
3602 | {
3603 | "text": "touring bikes",
3604 | "intent": "Explore",
3605 | "entities": [
3606 | {
3607 | "entity": "Detail",
3608 | "startPos": 0,
3609 | "endPos": 6
3610 | },
3611 | {
3612 | "entity": "Entity",
3613 | "startPos": 8,
3614 | "endPos": 12
3615 | }
3616 | ]
3617 | },
3618 | {
3619 | "text": "i like bikes",
3620 | "intent": "Explore",
3621 | "entities": [
3622 | {
3623 | "entity": "Entity",
3624 | "startPos": 7,
3625 | "endPos": 11
3626 | }
3627 | ]
3628 | },
3629 | {
3630 | "text": "i would like to buy myself a mountain bike",
3631 | "intent": "Explore",
3632 | "entities": [
3633 | {
3634 | "entity": "Detail",
3635 | "startPos": 29,
3636 | "endPos": 36
3637 | },
3638 | {
3639 | "entity": "Entity",
3640 | "startPos": 38,
3641 | "endPos": 41
3642 | }
3643 | ]
3644 | },
3645 | {
3646 | "text": "and where are you now?",
3647 | "intent": "None",
3648 | "entities": []
3649 | },
3650 | {
3651 | "text": "what other products do you have?",
3652 | "intent": "ShowTopCategories",
3653 | "entities": []
3654 | },
3655 | {
3656 | "text": "what about accessories?",
3657 | "intent": "Explore",
3658 | "entities": [
3659 | {
3660 | "entity": "Entity",
3661 | "startPos": 11,
3662 | "endPos": 21
3663 | }
3664 | ]
3665 | },
3666 | {
3667 | "text": "what do you have/",
3668 | "intent": "ShowTopCategories",
3669 | "entities": []
3670 | },
3671 | {
3672 | "text": "i’d like to look at fenders",
3673 | "intent": "Explore",
3674 | "entities": [
3675 | {
3676 | "entity": "Entity",
3677 | "startPos": 20,
3678 | "endPos": 26
3679 | }
3680 | ]
3681 | },
3682 | {
3683 | "text": "can i see my shopping cart?",
3684 | "intent": "ShowCart",
3685 | "entities": []
3686 | },
3687 | {
3688 | "text": "what's in my shopping cart?",
3689 | "intent": "ShowCart",
3690 | "entities": []
3691 | },
3692 | {
3693 | "text": "what's in my cart?",
3694 | "intent": "ShowCart",
3695 | "entities": []
3696 | },
3697 | {
3698 | "text": "show me my shopping cart please",
3699 | "intent": "ShowCart",
3700 | "entities": []
3701 | },
3702 | {
3703 | "text": "what's in my shopping cart",
3704 | "intent": "ShowCart",
3705 | "entities": []
3706 | },
3707 | {
3708 | "text": "show me some bikes",
3709 | "intent": "Explore",
3710 | "entities": [
3711 | {
3712 | "entity": "Entity",
3713 | "startPos": 13,
3714 | "endPos": 17
3715 | }
3716 | ]
3717 | },
3718 | {
3719 | "text": "show me my cart please",
3720 | "intent": "ShowCart",
3721 | "entities": []
3722 | },
3723 | {
3724 | "text": "show me components please",
3725 | "intent": "Explore",
3726 | "entities": [
3727 | {
3728 | "entity": "Entity",
3729 | "startPos": 8,
3730 | "endPos": 17
3731 | }
3732 | ]
3733 | },
3734 | {
3735 | "text": "ready to checkout",
3736 | "intent": "Checkout",
3737 | "entities": []
3738 | },
3739 | {
3740 | "text": "check me out please",
3741 | "intent": "Checkout",
3742 | "entities": []
3743 | },
3744 | {
3745 | "text": "all good and ready to checkout",
3746 | "intent": "Checkout",
3747 | "entities": []
3748 | },
3749 | {
3750 | "text": "all set and ready to checkout",
3751 | "intent": "Checkout",
3752 | "entities": []
3753 | },
3754 | {
3755 | "text": "will you show me my shopping cart?",
3756 | "intent": "ShowCart",
3757 | "entities": []
3758 | },
3759 | {
3760 | "text": "can you show me my cart?",
3761 | "intent": "ShowCart",
3762 | "entities": []
3763 | },
3764 | {
3765 | "text": "so what do you have?",
3766 | "intent": "ShowTopCategories",
3767 | "entities": []
3768 | },
3769 | {
3770 | "text": "what kind of components",
3771 | "intent": "Explore",
3772 | "entities": [
3773 | {
3774 | "entity": "Entity",
3775 | "startPos": 13,
3776 | "endPos": 22
3777 | }
3778 | ]
3779 | },
3780 | {
3781 | "text": "road bikes i think",
3782 | "intent": "Explore",
3783 | "entities": [
3784 | {
3785 | "entity": "Detail",
3786 | "startPos": 0,
3787 | "endPos": 3
3788 | },
3789 | {
3790 | "entity": "Entity",
3791 | "startPos": 5,
3792 | "endPos": 9
3793 | }
3794 | ]
3795 | },
3796 | {
3797 | "text": "what about lights?",
3798 | "intent": "Explore",
3799 | "entities": [
3800 | {
3801 | "entity": "Entity",
3802 | "startPos": 11,
3803 | "endPos": 16
3804 | }
3805 | ]
3806 | }
3807 | ]
3808 | }
--------------------------------------------------------------------------------