Drafts of SUSI Chatbots

While creating a SUSI bot using bot wizard, a user has the option to save a draft of the bot at any step. Then he/she can continue building the bot from there. We can do four operations on a draft: Storing, fetching, editing and deleting.

Storing a draft:

For the storage of drafts, a GET request is made to the storeDraft.json API. The code of chatbot is passed in ‘object’ parameter. This code must be in JSON format. A sample API call looks like this:

https://api.susi.ai/cms/storeDraft.json?object={"test":"123"}

The JSON passed in ‘object’ has the following format:

{
   “group”: /*group_here*/
   “language”: /*language_here*/
   “name”: /*bot_name_here*/
   “buildCode”: /*code_in_build_tab_here*/
   “designCode”: /*code_in_design_tab_here*/
   “configCode”: /*code_in_config_tab_here*/
}

While storing the draft, we can either specify an ID ourselves as a parameter named ‘id’ in the API call url or we can get a randomly generated id from the server itself.
The hex codes in designCode includes ‘#’ which can not be passed in the url. Hence, we remove it while saving a draft and then add it back while editing it.

Fetching a draft:

For fetching drafts, a GET request is made to the readDraft.json API. We can either simply call the API without any other parameters which will return all the chatbots of the user. The API call for this looks like this:

https://api.susi.ai/cms/readDraft.json

We can fetch a particular draft by specifying the ID of draft in the API call like this:

https://api.susi.ai/cms/readDraft.json?id=abcdef12

Editing a draft:

The drafts are displayed on the botbuilder page. From there a user can open a draft simply by clicking on it. This uses the ID of that draft to fetch its details by making a GET request to readDraft.json API as specified above.
For applying the details fetched to bot wizard, we pass the draft ID in url of bot wizard as “/botbuilder/botwizard?draftID=abcdef12”.  Then we fetch this ID using “this.getQueryStringValue(‘draftID’)” and this is followed by fetching all details of bot.
After fetching details of the chatbot, all the details are updated in the state of Botwizard component and hence user can continue making SUSI bot.

Deleting a draft:

For the deletion of drafts, a GET request is made to the deleteDraft.json API. We need to specify the id of the draft that we want to delete in the ‘id’ parameter. So, the API call looks like this:

https://api.susi.ai/cms/deleteDraft.json?id=abcdef12

Resources

Continue ReadingDrafts of SUSI Chatbots

Fetching Bots from SUSI.AI Server

Public skills of SUSI.AI are stored in susi_skill_cms repository. A private skill is different from a public skill. It can be viewed, edited and deleted only by the user of who created it. The purpose of private skill is that this acts as a chatbot. All the information of the bot like design, configuration etc is stored within the private skill itself. In order to show the lists of chatbots a user has created to him/her, we need to fetch these lists from the server. The API for fetching private skills is in ListSkillService.java servlet. This servlet is used for both the purposes for fetching public skills and private skills.

How are bots fetched?

All the private skills are stored in data/chatbot/chatbot.json location in the SUSI server. Hence, we can fetch these skills from here.
The API call to fetch private skills is:

https://api.susi.ai/cms/getSkillList.json?private=1&access_token=accessTokenHere

The API call has two parameters:

  1. private = 1: This parameter tells the servlet that we’re asking for private skills because same API is used for fetching public skills as well.
  2. access_token : We have to pass the access-token in order to authenticate the user and access the chatbots of the specific user because the chatbots are stored according to user id of the users in chatbot.json. To get the user id, we need access-token.

For fetching the chatbots from chatbot.json, we firstly authenticate the user. Then we check if the user has any chatbots or not. If the user has chatbots then we loop through chatbot.json to find the chatbots of user by using user id. After getting chatbots details, we simply present it in json structure. You can easily understand this through the following code snippets. For authenticating:

String userId = null;
String privateSkill = call.get("private", null);
if (call.get("access_token", null) != null) {
    ClientCredential credential = new ClientCredential(ClientCredential.Type.access_token, call.get("access_token", null));
    Authentication authentication = DAO.getAuthentication(credential);
    // check if access_token is valid
    if (authentication.getIdentity() != null) {
        ClientIdentity identity = authentication.getIdentity();
        userId = identity.getUuid();
    }
}

You can see that the parameter private is stored in privateSkill. We check if privateSkill is not null followed by checking if the user id provided is valid or not. When both the checks are successful, we create a JSON object in which we store all the bots. To do this, we loop through the chatbot.json file to find the user id provided in API call. If the user id doesn’t exist in chatbot.json file then a message “User has no chatbots.” is displayed. If the user id is found then we again loop through all the chatbots and put their name as the value of key “name”,  language as the value of key “language” and group as the value of key “group”. All these key value pairs for the chatbots are pushed in an array and finally that array is added to the JSON object. This JSON is then received as response of the API call. The following code will demonstrate this:

if(privateSkill != null) {
    if(userId != null) {
        JsonTray chatbot = DAO.chatbot;
        JSONObject result = new JSONObject();
        JSONObject userObject = new JSONObject();
        JSONArray botDetailsArray = new JSONArray();
        JSONArray chatbotArray = new JSONArray();
        for(String user_id : chatbot.keys())
        {
            if(user_id.equals(userId)) {
                userObject = chatbot.getJSONObject(user_id);
                Iterator chatbotDetails = userObject.keys();
                List<String> chatbotDetailsKeysList = new ArrayList<String>();
                while(chatbotDetails.hasNext()) {
                    String key = (String) chatbotDetails.next();
                    chatbotDetailsKeysList.add(key);
                }
                for(String chatbot_name : chatbotDetailsKeysList)
                {
                    chatbotArray = userObject.getJSONArray(chatbot_name);
                    for(int i=0; i<chatbotArray.length(); i++) {
                        String name = chatbotArray.getJSONObject(i).get("name").toString();
                        String language = chatbotArray.getJSONObject(i).get("language").toString();
                        String group = chatbotArray.getJSONObject(i).get("group").toString();
                        JSONObject botDetails = new JSONObject();
                        botDetails.put("name", name);
                        botDetails.put("language", language);
                        botDetails.put("group", group);
                        botDetailsArray.put(botDetails);
                        result.put("chatbots", botDetailsArray);
                    }
                }
            }
        }
        if(result.length()==0) {
            result.put("accepted", false);
            result.put("message", "User has no chatbots.");
            return new ServiceResponse(result);
        }
        result.put("accepted", true);
        result.put("message", "All chatbots of user fetched.");
        return new ServiceResponse(result);
    }
}

So, if chatbot.json contains:

{"c9b58e182ce6466e413d5acafae906ad": {"chatbots": [
 {
   "name": "testing111",
   "language": "en",
   "group": "Knowledge"
 },
 {
   "name": "DNS_Bot",
   "language": "en",
   "group": "Knowledge"
 }
]}}

Then, the JSON received at http://api.susi.ai/cms/getSkillList.json?private=1&access_token=JwTlO8gdCJUns569hzC3ujdhzbiF6I is:

{
  "session": {"identity": {
    "type": "email",
    "name": "test@whatever.com",
    "anonymous": false
  }},
  "chatbots": [
    {
      "name": "testing111",
      "language": "en",
      "group": "Knowledge"
    },
    {
      "name": "DNS_Bot",
      "language": "en",
      "group": "Knowledge"
    }
  ],
  "accepted": true,
  "message": "All chatbots of user fetched."
}

References:

Continue ReadingFetching Bots from SUSI.AI Server

How User preferences data is stored in Chat.susi.ai using Accounting Model

Like any other accounting services SUSI.AI also provides a lot of account preferences. Users can select their language, timezone, themes, speech settings etc. This data helps users to customize their experience when using SUSI.AI.

In the web client these user preferences are fetch from the server by UserPreferencesStore.js and the user identity is fetched by UserIdentityStore.js. These settings are then exported to the settings.react.js file. This file is responsible for the settings page and takes care of all user settings. Whenever a user changes a setting, it identifies the changes and show an option to save these changes. These changes are then updated on the server using the accounting model of SUSI.AI. Let’s take a look at each file discussed above in detail.

Continue ReadingHow User preferences data is stored in Chat.susi.ai using Accounting Model

Protected Skills for SUSI.AI Bot Wizard

The first version of SUSI AI web bot plugin is working like a protected skill. A SUSI.AI skill can be created by someone with minimum base user role USER, or in other words, anyone who is logged in.  Anyone can see this skill or edit it. This is not the case with a protected skill or bot. If a skill is protected it becomes a personal bot and then it can only be used by the SUSI AI web client (chatbot) created by that user. Also, only the person who created this skill will be able to edit it or delete it. This skill won’t be listed with all the other public skills on skills.susi.ai.

How is a skill made private?

To make a skill private, a new parameter is added to SusiSkill.java file. This is a boolean called protected. If this parameter is true (Yes) then the skill is protected else if the parameter is false (No) then the skill is not protected. To add protected parameter, we add the following code to SusiSkill.java:

private Boolean protectedSkill;
boolean protectedSkill = false;

You can see that protectedSkill is a boolean with initial value false.
We need to read the value of this parameter in skill code. This is done by the following code:

if (line.startsWith("::protected"&& (thenpos = line.indexOf(' ')) > 0) {
  if (line.substring(thenpos+ 1).trim().equalsIgnoreCase("yes")) protectedSkill=true;
  json.put("protected",protectedSkill);
}

As you can see that the value of protected is read from the skill code. If it’s ‘Yes’ then protectedSkill is set as true and if it’s ‘No’ then protectedSkill is set as false.
If no protected parameter is given in the skill code, its value remains false.

How to add only protected skills from bot wizard?

Now that protected parameter has been added to the server, we need to add this parameter in the skill code but it should be ‘Yes’ if the user is creating a bot and ‘No’ if user is creating a skill using skill creator. In order to do this, we simply check if the user is creating a bot wizard. This can be done by passing a props in the CreateSkill.js file when the skill creator is being used to create a protected skill. Next, we can determine whether the user is using bot wizard or not simply by an if else statement. The following code will demonstrate it:

if (this.props.botBuilder) {
 code = '::protected Yes\n' + code;
else {
 code = '::protected No\n' + code;
}

References:

Continue ReadingProtected Skills for SUSI.AI Bot Wizard

Offline support for Open Event Android with ROOM

So until now we were fetching data from the server and directly displaying it to the user in the Open Event Android app. There are several problems in this approach if the user changes the fragment and then returns to the same fragment he will have to fetch the data again, valuable network gets wasted. There is also no offline support. So we decided to introduce a local database in the app. ROOM was without a doubt our first choice

What is ROOM?

According to the official documentation,

The Room persistence library provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite.

The library helps you create a cache of your app’s data on a device that’s running your app. This cache, which serves as your app’s single source of truth, allows users to view a consistent copy of key information within your app, regardless of whether users have an internet connection.

Let’s get started

Integration of the ROOM database majorly consists of 3 steps

1 Create an entity

2 Create a DAO

3 Create a Database

That’s it, you are done !

Create the entity

There are just 2 requirements in order to make a model an entity

First use @Entity annotation on the model class to make it an entity. Secondly you need at least one field with @PrimaryKey annotation.

This entity class represents a table in database and all its fields are the columns of the table.

@Type("event")
@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy::class)
@Entity
data class Event(
@Id(LongIdHandler::class)
@PrimaryKey
val id: Long,
val name: String,
val identifier: String,
val startsAt: String)

Create DAO

Data Access Object or DAO for short are used to tell the database how to to put the data.

We can use the following annotations to perform simple SQL queries in ROOM

@Insert, @Update, @Delete for proper actions: inserting, updating and deleting records

@Query for creating queries — we can make select from the database

@Dao
interface EventDao {
@Insert(onConflict = REPLACE)
fun insertEvents(events: List<Event>)

@Insert(onConflict = REPLACE)
fun insertEvent(event: Event)

@Query("DELETE FROM Event")
fun deleteAll()

@Query("SELECT * from Event ORDER BY startsAt DESC")
fun getAllEvents(): Flowable<List<Event>>

@Query("SELECT * from Event WHERE id = :id")
fun getEvent(id: Long): Flowable<Event>
}

Create the Database

We need to create a public abstract class that extends RoomDatabase

After that annotate the class to be a Room database, declare the entities that belong in the database and set the version number. Listing the entities will create tables in the database.

Define the DAOs that work with the database. Make the database a singleton to prevent having multiple instances of the database opened at the same time. A lot has been said let’s have a look at the code now.

@Database(entities = [Event::class, User::class], version = 1)
abstract class OpenEventDatabase : RoomDatabase() {

abstract fun eventDao(): EventDao

abstract fun userDao(): UserDao
}
Room.databaseBuilder(androidApplication(),
OpenEventDatabase::class.java, "open_event_database")
.fallbackToDestructiveMigration()
.build()

The important thing here is that all operations must be done in the background thread. You can do it by using AsyncTask, Handler, RxJava or anything else.

Resources

  1. Room Official Documentation : https://developer.android.com/topic/libraries/architecture/room
  2. Google Code Lab :https://codelabs.developers.google.com/codelabs/android-room-with-a-view/#0
Continue ReadingOffline support for Open Event Android with ROOM

Getting results for Multi-word Query and showing limited results in Knowledge Graph

In Susper knowledge graph,we were unable to get results for multi word query from Wikipedia API and also we were unable to decide how much information should be shown in knowledge graph which was retrieved from Wikipedia API.For example for a query donald trump we do not get information (https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro=&explaintext=&titles=donald%20trump) . Also for searching for any query like india (https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro=&explaintext=&titles=india) we get a lot of information. Earlier we used Angular slice pipe to display only 600 characters in knowledge graph (Infobox) but almost for all queries the sentences were terminated before it was completed. In this blog, I will describe how we solved both problems in knowledge graph.

Getting results for multi word query from Wikipedia API:

On searching a lot we found that the results were present on Wikipedia for multi word queries but these queries must have its starting letters as capital letters. For example we do not get results for queries such as donald trump

i.e https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro=&explaintext=&titles=donald%20trump

but when we make a query for Donald Trump  

i.e

https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro=&explaintext=&titles=Donald%20Trump  

But since in most of the search queries users use small letters, we need to convert the query such that each word starts with a capital letter. Since the Knowledge Graph has been implemented using ngrx pattern, this logic was easily implemented in knowledge effects. For this we will be selecting each word in query by using regular expression and then we will use toUppercase() and toLowerCase() methods of javascript and will capitalize every word of the query.

toTitleCase(str) {
    return str.replace(
        /\w\S*/g,
        function(txt) {
            return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
        }
    );
}

this.knowledgeservice.getSearchResults(this.toTitleCase(querypay.query))

 

Like this we now get results for almost all queries.

Limiting information in Knowledge Graph without terminating the sentences:

To solve this issue, we decided to show only four lines of data retrieved from Wikipedia API

For this we have implemented a getPosition function which will take three parameters a string, a substring and a index. Here string is the whole string from which the function will return the index of index position substring.

getPosition(string, subString, index) {
    return string.split(subString, index).join(subString).length;
 }
this.description = this.description.slice(0, this.getPosition(this.description, '.', 4) + 1);

 

Here, We get the index of 4th ‘.’ full stop by getPosition() function and then pass it to javascript slice method to slice the string upto that position.

Using this we limited results to 4 lines without terminating each line in middle.

References

1.W3Schools Regular Expression: https://www.w3schools.com/jsref/jsref_obj_regexp.asp

2.Javascript Slice method: https://www.w3schools.com/jsref/jsref_slice_array.asp

3.Wikipedia API: https://www.mediawiki.org/wiki/API:Main_page

Continue ReadingGetting results for Multi-word Query and showing limited results in Knowledge Graph

Displaying All Providers and All Authors separately in Susper Angular App

In Susper results are retrieved from a lots of providers and authors. Displaying all authors’ and providers’ list on result page make appearance of result section odd and more complex. In this blog, I will describe how we have separated the list of all providers and authors from main result page and displayed it on a lightbox.

Steps:

  • Making a black overlay for lightbox:

First we of all to display the results on a lightbox,we to have a black overlay for this we need a div element and inside it we need a closing button.

Clicking on the black overlay should toggle status of light box therefore we have binded click event with showMore() method.

<div id="fade_analytics" class="black_overlay" (click)='showMore()'><a class="closeX" id="closeX">&#10006;</a></div>
  • Creating a white content to display all Providers or Authors:

Now we need a white block to display all Providers or all Authors so after creating a black overlay in background we need a white foreground.

Therefore I have implemented a div element and put all the chart elements inside the div element so that it will be displayed on a white foreground on a translucent black background.

<div id="light_analytics" class="white_content_analytics">
<div class="show_more"><h2 class="heading"><b>Analytics</b></h2><div id="filtersearch" *ngFor="let nav of navigation$|async; let i = index"><div class="card-container" id="relate" *ngIf="nav.displayname =='Provider' || nav.displayname =='Authors'"><h3  class="related-searches" *ngIf="i===currentelement"><b>Top {{nav.displayname}}<span *ngIf="nav.displayname.slice(-1)!=='s'">s</span></b></h3>
<div style="display: block;cursor:pointer;cursor: hand;" *ngFor="let element of  ((i != currentelement)? (nav.elements | slice:0:0) :nav.elements)">
</div></div></div></div>

The code to display All providers and All authors are contained inside the   white block and the logic to display the each section is also implemented.

  • Showing only 50 results on result page and implementing a button to open all providers or authors in light box:

Currently we are displaying the top five providers and authors and we provide a button to show more providers and authors which then displays 50 more results and then we display an interactive button that toggles display of all authors and providers on a lightbox. To display 5 and 50 results we use slice pipe in angular. The button Show All Providers/Authors is binded with showMore() function which toggles lightbox.

<div style="display: block;cursor:pointer;cursor: hand;" *ngFor="let element of ((i != selectedelement)? (nav.elements | slice:0:5) :(nav.elements | slice:0:50) )">
<a [routerLink]="['/search']" [queryParams]="changequerylook(element.modifier,element)" *ngIf="element.name" href="#" [style.color]="themeService.linkColor">
{{element.name}} <span class="badge badge-info">{{element.count}}</span>
</a><a  *ngIf="!element.name" style="color: black;cursor: default" href="#" [style.color]="themeService.linkColor">
Undefined Author <span class="badge badge-info">{{element.count}}</span></a></div>
<button class="showMoreBtn" (click)="showMore()" *ngIf="selectedelement===i" [style.color]="themeService.linkColor">Show All {{nav.displayname}}<span *ngIf="nav.displayname.slice(-1)!=='s'">s</span></button>

 

  • Implementing the logic to display all results on white content:

Now we need to implement the logic to toggle lightbox to display All Providers and All Authors. This logic is implemented mainly in showMore() function which toggles the lightbox containing the All Providers and All Authors

showMore() { this.selectedelement = -1;
if (!this.showMoreAnalytics) { this.showMoreAnalytics = true; document.getElementById('light_analytics').style.display = 'block'; document.getElementById('fade_analytics').style.display = 'block'; } else { this.showMoreAnalytics = false;
document.getElementById('light_analytics').style.display = 'none';
document.getElementById('fade_analytics').style.display = 'none';}}

Here we use showMoreAnalytics variable to check whether the user has asked to show all Providers or Authors. We also make selectedelement variable to -1 to hide the long list of 50 Providers or Authors when Lightbox displays all the Providers or Author. Finally we use document object in javascript and select the DOM elements by their id and set their display property this shows and hides the lightbox on result page.

Resources

1.YaCy Advanced Search Parameters

http://www.yacy-websuche.de/wiki/index.php/En:SearchParameters

2.W3School lightbox

https://www.w3schools.com/howto/howto_js_lightbox.asp

 

Continue ReadingDisplaying All Providers and All Authors separately in Susper Angular App

Adding New tests for Knowledge Service

Testing is done to test our application by executing some functions by creating instances of corresponding classes,executing functions and checking the actual behaviour of our app with expected result. The tools and frameworks used in Angular are Jasmine and Karma. In this blog, I will describe about how I have implemented tests for Newly Added Knowledge API service that helped us to increase overall code coverage by 1.05% in Susper .

Adding tests for Knowledge API:

We need to check the API that whether it is functioning or not and this can be done by using a mocked response (hardcoded response) for any query and then comparing this with the received response from the API. This will help us to check proper functioning of our API.

This is a common practice in Angular and to achieve this we will be using some dependencies like MockBackend, MockConnection, BaseRequestOptions provided by Angular.

import { MockBackend, MockConnection } from '@angular/http/testing';
import { Http, Jsonp, BaseRequestOptions, RequestMethod, Response, ResponseOptions, HttpModule, JsonpModule } from '@angular/http';

 

We will also need to define a Mock response, here I have used hard coded response for query India. Here is the mocked response.

export const MockKnowledgeApi = {
    results: [
        {"batchcomplete": "", "query":
        {"normalized":
        [{"from": "india", "to": "India"}], "pages":
        {"14533": {"pageid": 14533, "ns": 0, "title": "India", "extract": `India (IAST: Bh\u0101rat), also called the Republic of India (IAST: Bh\u0101rat Ga\u1e47ar\u0101jya), is a country in South Asia.
   }}}} ], MaxHits : 5 };

 

Now we will use a Mock constant mock_Http_provider for http and will inject instances of MockBackend and BaseRequestOptions.

const mockHttp_provider = {
  provide: Http,
  deps: [MockBackend, BaseRequestOptions],
  useFactory: (backend: MockBackend, options: BaseRequestOptions) => { return new Http(backend, options); } };

 

Now we need to add all the services and dependencies which we will be using in providers and we will inject the instances of Knowledgeapi Service and MockBackend in beforeEach function.

beforeEach(inject([KnowledgeapiService, MockBackend], (knowledgeService: KnowledgeapiService, mockBackend: MockBackend) => { service = knowledgeService;
    backend = mockBackend;}));

 

Now we will use the same query for which we have created the mocked response and we will be checking the response of from our API.

const searchquery = 'india';
connection.mockRespond(new Response(options));
      expect(connection.request.method).toEqual(RequestMethod.Get);
      expect(connection.request.url).toBe(   `https://en.wikipedia.org/w/api.php?&origin=*&format=json&action=query&prop=extracts&exintro=&explaintext=&` +
                    `titles=${searchquery}`);});
service.getSearchResults(searchquery).subscribe((res) => {
      expect(res).toEqual(MockKnowledgeApi);});

 

This will check the working of our API and if it is working then our test case will pass.

Like this we have implemented the tests for Knowledgeapi Service which helped us to test our API and increase overall code coverage significantly.

Resources

  1. Testing in Angular:https://angular.io/guide/testing
  2. Testing by Mockbackend:https://angular.io/api/http/testing/MockBackend
  3. Using MockBackend to simulate response: https://codecraft.tv/courses/angular/unit-testing/http-and-jsonp/#_using_the_code_mockbackend_code_to_simulate_a_response
Continue ReadingAdding New tests for Knowledge Service

Showing Query Description in Infobox

Knowledge Graph is used to give a brief introduction about the search query instantly. But the information of knowledge graph becomes more useful when it is defined by one or two line description. Google currently shows a description of search query in its knowledge graph which makes the search results more meaningful. In this blog I will describe how I have description for query using Wikidata API implemented a similar functionality in Susper.

Steps:

1.Adding a function to fetch data from Wikidata in Service:

getQueryDescription(searchquery) {
   const params = new URLSearchParams();
   params.set('origin', '*');
   params.set('action', 'wbsearchentities');
   params.set('search', searchquery);
   params.set('language', 'en');
   params.set('format', 'json');
   const headers = new Headers({ 'Accept': 'application/json' });
   const options = new RequestOptions({ headers: headers, search: params });
   return this.http.get(this.descriptionURL, options).map(res =>
     res.json()
     ).catch(this.handleError);
 }

 

2.Creating an effect for query description:

To implement the logic of getting query description from the results returned by the service is made in knowledge.ts file which creates an effect for knowledge description.

Here is the effect for query description

this.knowledgeservice.getQueryDescription(querypay.query)
       .takeUntil(nextSearch$)
       .subscribe((response) => {
         if (response['search']) {
           this.store.dispatch(new knowledge.DescriptionAction(response));
           return empty();
            } else {    this.store.dispatch(new knowledge.DescriptionAction([]));
                         return empty();
                 }
});

 

The response is then dispatched to the corresponding knowledge action.

3.Creating an Action for Query description:

After dispatching the query description logic to the action, we must have an action which will correspond to get the query description. Here is the implementation of Query Description Action in knowledge.ts action file.

export const ActionTypes = {
 CONTENT_CHANGE: type('[Knowledge] Content Change'),
 IMAGE_CHANGE: type('[Knowledge] Image Change'),
 DESCRIPTION_CHANGE: type('[Knowledge] Description Change')
};
export class SearchContentAction implements Action {
 type = ActionTypes.CONTENT_CHANGE;
 constructor(public payload: object) {}
}
export class SearchImageAction implements Action {
 type = ActionTypes.IMAGE_CHANGE;
 constructor(public payload: object) {}
}
export class DescriptionAction implements Action {
 type = ActionTypes.DESCRIPTION_CHANGE;
 constructor(public payload: object) {}
}
export type Actions
 = SearchContentAction | SearchImageAction | DescriptionAction;

 

DescriptionAction is the action for Query Description and is then exported for the reducer.

4.Creating a case for query description in Knowledge reducer:

We must have a case to deal in reducer function to deal with query description action and here is the code for the case:

case knowledge.ActionTypes.DESCRIPTION_CHANGE: {
     const description_response = action.payload;
     return Object.assign({}, state, {
       content_response: state.content_response,
       image_response: state.image_response,
       description_response: description_response
     });
   }

 

The new state is then exported to the store:

export const getDescriptionResponse = (state: State) => state.description_response;

 

5.Updating the contents in store:

To update the store we just retrieve the getKnowledgeState and read its description_response and export it.

export const getDescriptionResponse = (state: State) => state.description_response;

 

6.Fetching Query Description from store in infobox component:

Now fetching results is very simple, we just need to select the getDescription from the store at store it in description_response$ and then we extract the query description and store in a local variable querydescription

this.description_response$ = store.select(fromRoot.getDescription);
   this.description_response$.subscribe(res => {
     if (res['search']) {
       this.querydescription = res['search'][0]['description'];
     }
   }
   );

 

7.Displaying query description on the result page:

Now displaying the query description on our page is very simple we just need to use our querydescription variable.

<p class="query_description">{{this.querydescription}}</p><hr width="50%" align="left" *ngIf="this.image">

Resources

  1. Wikidata API: https://www.wikidata.org/w/api.php
  2. Angular Services:  https://angular.io/tutorial/toh-pt4
  3. Corresponding Pull Request: https://github.com/fossasia/susper.com/pull/1115

 

 

 

Continue ReadingShowing Query Description in Infobox

Fetching responses from SUSI.AI Server for Botbuilder Build Views

In SUSI.AI, we use skill editor for creating and editing public/private skills. The editor we use is Ace editor. The skill is written in a code format documented here. This works fine for a developer but for a user with little experience in coding, this can be confusing. Hence, for providing more clarity as to what the skill does, I built conversation view and tree view along with code view for the skill editor.

Conversation view shows the skill in form of actual conversation between the user and the bot while tree views shows the same conversation in form of a tree. Earlier these views were implemented by converting the code view into an object containing user queries and SUSI responses.

While this works for simple skills, it obviously won’t work for a complex skill in which the responses are fetched from an API. Hence we needed live responses from SUSI server.

This is done similar to how preview works. We pass the whole skill in an instant parameter in the chat.json API along with the user query. This gives us the response from SUSI in form of a JSON.

API Call:

We send a GET request to the following URL:

https://api.susi.ai/susi/chat.json?q=userQuery&instant=wholeSkill

This contains two parameters:

  • q: The user query is passed in this parameter.
  • instant: The whole skill code (present in the code view) is passed in this parameter.

The response is a JSON providing response from SUSI.

Getting user queries:

We can not rely on user to provide the user queries in conversation view and tree view because the user has already provided it in the code view. Hence, we fetch the user queries from code view. This is simply done by dissecting the code and putting all the lines which don’t start with ::, !, #, {, } and “ in an array. Then we split the entries of this array wherever a vertical bar (|) is found. This provides us an array containing all the user queries. It’ll be clear from the following function:

fetchUserInputs = () => {
  let code = this.state.code;
  let userInputs = [];
  let userQueries = [];
  var lines = code.split('\n');
  for (let i = 0; i < lines.length; i++) {
    let line = lines[i];
    if (
      line &&
      !line.startsWith('::') &&
      !line.startsWith('!') &&
      !line.startsWith('#') &&
      !line.startsWith('{') &&
      !line.startsWith('}') &&
      !line.startsWith('"')
    ) {
      let user_query = line;
      while (true) {
        i++;
        if (i >= lines.length) {
          break;
        }
        line = lines[i];
        if (
          line &&
          !line.startsWith('::') &&
          !line.startsWith('!') &&
          !line.startsWith('#')
        ) {
          break;
        }
      }
      userQueries.push(user_query);
    }
  }
  for (let i = 0; i < userQueries.length; i++) {
    let queries = userQueries[i];
    let queryArray = queries.trim().split('|');
    for (let j = 0; j < queryArray.length; j++) {
      userInputs.push(queryArray[j]);
    }
  }
  this.setState({ userInputs }, () => this.getResponses(0));
};

Getting response to a single query at a time:

Now, we have an array containing all the user queries but we can not simply run a loop through the array and then get responses for each query because the AJAX call that we’re making to fetch response is asynchronous. Hence, this will result in multiple AJAX calls in a very short period of time. This will cause a failure in fetching responses and conversation view won’t work. We definitely don’t want that.

To solve this problem, we get response for a single query at a time and make the next AJAX call only when the response for the current call is received. You can see in the code snippet provided in last section, after updating state of userInputs, we’re calling getResponses as a callback and passing 0. This 0 is the index of array which will be incremented on every successful AJAX call. The following code snippet will demonstrate this:

$.ajax({
  type: 'GET',
  url: url,
  contentType: 'application/json',
  dataType: 'json',
  success: function(data) {
  let answer;
  if (data.answers[0]) {
    // Putting response in an object along with user query
    if (responseNumber + 1 === userInputs.length) { // Stopping when responses are fetched for all user queries.
      this.setState({ loaded: true });
    }
    this.setState({ responseData }, () => // updating the response data
      this.getResponses(++responseNumber),  // Incrementing the index and calling getResnponses again as a callback when response data state is updated.
    );
  }.bind(this),
  error: function(err) {
    console.log(err);
  }.bind(this),
});

The code snippets I provided are used in conversation view. Same algorithm is used in tree view as well.

References:

Continue ReadingFetching responses from SUSI.AI Server for Botbuilder Build Views