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

Different views for SUSI Skill Creator

SUSI Skill Creator is a service provided to easily create skills for SUSI. The skill can be written in the form of code. The coding syntax is described in SUSI skill tutorial. The problem with this is that we can’t expect everyone to be great coders and be able to understand the document and easily create a skill. Hence, we needed some robust alternatives for people who don’t want to write the code. This is where UI View and Tree View comes in.

What is UI View?

UI View or User Interface View shows the skill in form of chat bubbles. This is useful for demonstrating how the actual chat will look like.

Hello
Hi. I’m SUSI.

The following code will be shown as this in conversation view:

What is Tree View?

Tree view shows the conversation in form of a tree with its branching representing the chats.

Hello|Hi
Hi. I'm SUSI.|Hey there!

The following code will be shown as this in tree view:

How are the views synchronised?

We’re basically not making any API calls or performing any major function in any of the views. The components of different views are solely for their own function. All the API calls are made in a parent component named SkillCreator.

The main reason for creating a parent component is that we need the category, language, name and commit message options to appear on all the three views. Hence, it only makes sense to have a parent component for them and different components for the views.

The default code (that appears in code editor by default) is in a state in SkillCreator. We pass this state to the code view component as a props.

<CodeView
  skillCode={this.state.code}
  sendInfoToProps={this.sendInfoToProps}
/>

Now, we need to get any change made in the code in CodeView and change the code state of SkillCreator accordingly. For that, we have a function named sendInfoToProps which is passed as props to CodeView and is then called from CodeView whenever we make some change. This will be more clear after having a look at the function sendInfoToProps:

sendInfoToProps = value => {
 if (value) {
  this.setState(
   {
     code: value.code ? value.code : this.state.code,
     expertValue: value.expertValue
       ? value.expertValue
       : this.state.expertValue,
     groupValue: value.groupValue
       ? value.groupValue
       : this.state.groupValue,
     languageValue: value.languageValue
       ? value.languageValue
       : this.state.groupValue,
   imagUrl: value.imageUrl ? value.imageUrl : this.state.imageUrl,
   },
   () => this.generateSkillData(),
  );
 }
};

You can see that we update the code in SkillCreator and other states required. Also, you can notice the we’re calling generateSkillData function here. This is done to convert the code to skill data which is sent to Conversation view and Tree view as props.
We’re calling generateSkillData as a callback function because setState is an asynchronous function.
This will demonstrate how this is passed to Conversation view:

<ConversationView
  skillData={this.state.skillData}
  handleDeleteNode={this.handleDeleteNode}
/>

Same is the case for this handleDeleteNode function. We have an option in both Conversation view and Tree view to allow the user to delete a conversation (originally written as skill in code view). On delete, handleDeleteNode is called from the props and the conversation is deleted from the code which is then send to all the views (as props) and the views are updated as well.

References:

Continue ReadingDifferent views for SUSI Skill Creator

Code view and UI View in design tab of bot wizard

Design tab is for designing your SUSI AI chatbot. You can change the background colour, add an image as background color, change colour of user text box, bot text box etc. On SUSI AI bot wizard design tab, we have two views. These views show the same thing but in a different way for different people. The code view is mainly for the developers while the UI (User Interface) view is for a non-developer user because we can not expect them to know how to code even if it is easy.

Code View

The current code view of design tab looks like this:

::bodyBackground #ffffff
::bodyBackgroundImage
::userMessageBoxBackground #0077e5
::userMessageTextColor #ffffff
::botMessageBoxBackground #f8f8f8
::botMessageTextColor #455a64
::botIconColor #000000
::botIconImage

For changing colours, you simply have to change the hex codes and for adding images as background, you need to add the url of image. You can also upload an image but that function is provided in the UI View.

UI View

The current UI view of design tab looks like this:

Here, we have colour picker for user to choose a colour for various components of the chatbot. There’s also a small box with the same colour as specified in hex code for previewing.

Switching and Synchronising between Code view and UI view

We have different components for code view and UI view. In order to synchronise both the views, we need to have the same code in state of both components. To do that, we add the code in state of the parent component i.e. design.js and then pass it to code view and UI view as states.

The following code snippet will demonstrate this:

<CodeView
    design={{
        sendInfoToProps: this.sendInfoToProps,
        updateSettings: this.updateSettings,
        code: this.state.code,
    }}
/>

You can see that we pass a whole object as props.
Using this approach, same code is passed to both the views. This solves the first problem. The next problem is that if we do a change in code view then it should happen in the code state of parent file as well. Same goes for UI view.
To do this, whenever we change the code in code view or UI view, we call a function in the parent file (design.js) which was passed as props and we pass the state of code view or UI view as arguments of this function. This function then updates the state accordingly. The name of the function is sendInfoToProps and it is as follows:

sendInfoToProps = values => {
    this.setState({ ...values });
};

You can see that it simply updates the state value as per the parameters passed to it.
So this is how synchronisation works between different views.

References:

Continue ReadingCode view and UI View in design tab of bot wizard

Countrywise Usage Analytics of a Skill in SUSI.AI

The statistics regarding which country the skills are being used is quite important. They help in updating the skill to support the native language of those countries. SUSI.AI must be able to understand as well as reply in its user’s language. So mainly the server side and some client side (web client) implementation of country wise skill usage statistics is explained in this blog.

Fetching the user’s location on the web client

  1. Add a function in chat.susi.ai/src/actions/API.actions.js to fetch the users location. The function makes a call to freegeoip.net API which returns the client’s location based on its IP address. So country name and code are required for country wise usage analytics.

export function getLocation(){
  $.ajax({
    url: 'https://cors-anywhere.herokuapp.com/http://freegeoip.net/json/',
    success: function (response) {
      _Location = {
        lat: response.latitude,
        lng: response.longitude,
        countryCode: response.country_code,
        countryName: response.country_name
      };
    },
  });
}
  1. Send the location parameters along with the query while fetching reply from SUSI server in chat.json API. The parameters are country_name and country_code.

if(_Location){
	url += '&latitude='+_Location.lat+'&longitude='+_Location.lng+'&country_code='+_Location.countryCode+'&country_name='+_Location.countryName;
}

Storage of Country Wise Skill Usage Data on SUSI Server

  1. Create a countryWiseSkillUsage.json file to store the country wise skill usage stats and make a JSONTray object for that in src/ai/susi/DAO.java file. The JSON file contains the country name, country code and the usage count in that country.
  1. Modify the src/ai/susi/server/api/susi/SusiService.java file to fetch country_name and country_code from the query parameters and pass them SusiCognition constructor.

String countryName = post.get("country_name", "");
String countryCode = post.get("country_code", "");
...

SusiCognition cognition = new SusiCognition(q, timezoneOffset, latitude, longitude, countryCode, countryName, language, count, user.getIdentity(), minds.toArray(new SusiMind[minds.size()]));
  1. Modify the src/ai/susi/mind/SusiCognition.java file to accept the countryCode and countryName in the constructor parameters. Check which skill is being currently used for the response and update the skill usage stats for that country in countryWiseSkillUsage.json. Call the function updateCountryWiseUsageData() to update the skill usage data.

if (!countryCode.equals("") && !countryName.equals("")) {
    List<String> skills = dispute.get(0).getSkills();
    for (String skill : skills) {
        try {
            updateCountryWiseUsageData(skill, countryCode, countryName);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The updateCountryWiseUsageData() function accepts the skill path , country name and country code. It parses the skill path to get the skill metadata like its model name, group name, language etc. The function then checks if the country already exists in the JSON file or not. If it exists then it increments the usage count by 1 else it creates an entry for the skill in the JSON file and initializes it with the current country name and usage count 1.

for (int i = 0; i < countryWiseUsageData.length(); i++) {
  countryUsage = countryWiseUsageData.getJSONObject(i);
  if (countryUsage.get("country_code").equals(countryCode)) {
    countryUsage.put("count", countryUsage.getInt("count")+1);
    countryWiseUsageData.put(i,countryUsage);
  }
}

API to access the Country Wise Skill Usage Data

  1. Create GetCountryWiseSkillUsageService.java file to return the usage stats stored in countryWiseSkillUsage.json

public ServiceResponse serviceImpl(Query call, HttpServletResponse response, Authorization rights, final JsonObjectWithDefault permissions) {
        
  ...  // Fetch the query parameters
  JSONArray countryWiseSkillUsage = languageName.getJSONArray(skill_name);
  return new ServiceResponse(result);
}
  1. Add the API file to src/ai/susi/server/api/susi/SusiServer.java

services = new Class[]{
	...
	//Skill usage data
	GetCountryWiseSkillUsageService.class
	...
}

 

Endpoint : /cms/getCountryWiseSkillUsage.json

Parameters

  • model
  • group
  • language
  • Skill

Sample query: /cms/getCountryWiseSkillUsage.json?model=general&group=Knowledge&language=en&skill=aboutsusi

Sample response:

{  
   "skill_usage":[  
      {  
         "country_code":"MYS",
         "country_name":"Malaysia",
         "count":1
      },
      {  
         "country_code":"MYS",
         "country_name":"Malaysia",
         "count":1
      }
   ],
   "session":{  
      "identity":{  
         "type":"host",
         "name":"162.158.154.147_81c88a10",
         "anonymous":true
      }
   },
   "skill_name":"ceo",
   "accepted":true,
   "message":"Country wise skill usage fetched"
}

Resources

Continue ReadingCountrywise Usage Analytics of a Skill in SUSI.AI

Converting Drupal into WordPress

Converting Drupal into WordPress

WordPress has the ability to update automatically, while our existing Drupal systems require time to take care manually. This is the main reason for our switch.

So the first thing I did was to google for a plugin, I found about the fg-drupal2wordpress plugin however it only passes the posts, and doesn’t import users/media/tags unless you pay about 40€, so naturally I stayed off from it, my second intuition was to look it up on Github, and what I found it was also a plugin and it was open source, I used it, and it worked for almost anything except the images.

original website, even some images are missing now because the website is not on /

The gallery

Challenges

The thing with the images is that the Drupal that I was given used a very old version of the drupal “Images” add-on, so the plugin converts these images as “posts”, but they’re not actually converted as images.

as you can see, some posts are blank

In drupal, every blogpost and image are stored as a Node

sql query result of “SELECT nid,file_usage.fid,node.title,files.filepath FROM perspektive.node INNER JOIN perspektive.file_usage ON perspektive.node.nid = perspektive.file_usage.id INNER JOIN perspektive.files ON perspektive.file_usage.fid = perspektive.files.fid”, as you can see we can see where the images are stored, what node id do they have, and what file id they have according to Drupal.

In order to pass these images to wordpress you need a script that reads every image location, copies it into the correspondent wp-content/uploads/YYYY/MM/ directory, makes a thumbnail of every image, and you have to add around 5 columns to the wp_postmeta table, if you do that, you’ll effectively import all the images to the media file in wordpress, and that is without counting the tags, and the author of every image! It is not an easy task!

Proposals

One solution to this problem was to add an `img` tag for every of these posts so that every posts has their own image, and this works in principle, you might even add an “image” tag to every image post, however they’re not actually appended on the wordpress media library, so if you want a page to act as a gallery, it won’t work in that sense. What would you need to do is to add every image as a media gallery on wordpress, you can do this, but you have to convert a lot of images, and it’s not easy because you also have to create thumbnails, you could kind of automate this process using WP-CLI, and add a lot of images with it, the problem would be that the images keep their post id or node id on drupal, and copying the information that is contained on the drupal database, it’s not hard but it takes time to do.

Another one was to actually update the open source plugin myself which is what I started to do, so I started digging into its code to find out how it worked, I probably found some errors while investigating, and I also searched for forks of the project since apparently the original author was not replying to PRs or Issues, the project had been abandoned, so if I needed help, I could only help myself.

Thoughts

I always knew about CMS, and how they worked, but never got into them because I never felt I was fully of control or that I understood the website, however now that I investigated about this issue, I feel I have a more intrinsic understanding about Drupal and WordPress, and I might be more prompt to use them myself in the case that I need to.

I find it surprising that it is really hard to find converters or conversors for SQL to other formats (like JSON), there were some web converters but they were web only and couldn’t handle the data size. At the end I used PHPMyAdmin, which was probably what I was looking for, and I did learn a ton about SQL.

Resources

Author: Mario Behling, Website: https://web.archive.org/web/20060615070835/http://www.perspektive89.com:80, Title: Perspektive89

Author: Dries Buytaert, https://api.drupal.org/api/drupal, Title: Drupal API reference

Author: WordPress, Website: https://developer.wordpress.org/reference/, Title: WordPress Reference

Continue ReadingConverting Drupal into WordPress

Skills tab of SUSI AI Admin Panel

The Skills tab in SUSI.AI Admin Panel displays all the skills of SUSI in a tabular form. The table is created using the Table component of Ant Design. It’s preferred because we’re going to handle a lot of data here and that can turn out to be heavy if we use Google’s Material-ui.

Displaying the data:

The data is rendered in the form of a table which has seven columns – Name, Group, Language, Type, Author, Status and Action. The first 5 columns displays basic details of a skill. The “Status” column shows whether the skill has been reviewed by admin or not. All the reviewed skills are either “Approved” or “Not Approved”. The final column “Action” contains all the actions that admin can perform on the skill. Currently, an admin can change the review status of a skill. More actions will be added in future as this is still in beta.

All the column names are stored in a variable in the form of an array. The following code will demonstrate the way of making three columns – Name, Group, Language.

this.columns = [
  {
    title: 'Name',
    dataIndex: 'skill_name',
    sorter: false,
    width: '20%',
  },
  {
    title: 'Group',
    dataIndex: 'group',
    width: '15%',
  },
  {
    title: 'Language',
    dataIndex: 'language',
    width: '10%',
  },
];

All the skills are also stored in an array which is a state variable. These columns and skills are then passed to the Table component as props. The following code will demonstrate that:

<Table
  columns={this.columns}
  rowKey={record => record.registered}
  dataSource={this.state.skillsData}
/>

Fetching all the skills from SUSI Server:

All the public skills can be fetched from ListSkillService API of SUSI Server. We can filter all the skills using various filters. We want to display the skills alphabetically on Skills tab in Admin Panel. Hence, we filter the skills accordingly. To do this, we pass three parameters in the GET request. They are as follows:

  • applyFilter: true
  • filter_name: ascending
  • filter_type: lexicographical

This returns all the skills in an alphabetical order. The API request url looks like this:

https://api.susi.ai/cms/getSkillList.json?applyFilter=true&filter_name=ascending&filter_type=lexicographical

After fetching the skills, we put all the parameters of these skills required for our table in an object which is then pushed into an array. One object is created for each skill.
If the API call fails for some reason then a Google’s Material-ui Snackbar appears with a message that an error occurred.

Changing review status of a skill:

An admin can change the review status of any skill. This is done by making a GET request to ChangeSkillStatusService API. The request contains five parameters. They are as follows:

  1. Model: Model of the skill is passed here (string)
  2. Group: Group of the skill is passed here (string)
  3. Language: Language of the skill is passed here (string)
  4. Reviewed: true is passed is skill has been approved and false if skill is not approved. (boolean)
  5. Access_token: The access token of user is passed here. This is for verifying the user’s BASE ROLE. This is taken from cookies using cookies.get(‘loggedIn’). (string)

The call url looks like this:

https://api.susi.ai/cms/changeSkillStatus.json?model=general&group=Knowledge&language=en&skill=aboutsusi&reviewed=true&access_token=yourAccessTokenHere

If the API call is successful, then the review status of a skill is successfully changed. Otherwise, an error message is thrown.

References:

Continue ReadingSkills tab of SUSI AI Admin Panel

Code view in Configure tab of SUSI.AI Bot Wizard

The purpose of configure tab in SUSI.AI bot wizard is to provide the bot creator various options on how the bot will interact with different websites. It currently provides an option of enabling or disabling the chatbot on different websites.
The configure tab has two parts. One is the code view which allows the users to write websites on which they want to enable/disable the chatbot and a table below it which lists those websites.

The default code in code view is passed in a state in the Configure.js file. The following code demonstrates that:

this.state = {
  code: this.props.code,
};

Fetching data from code view:

After writing the websites on which the user wants the chatbot to be enabled/disabled and pressing on the ‘Save’ button, a functiongenerateConfigData is called.
This function takes the code in code view and stores in inside of a variable. Then it splits the code and makes two arrays:

  • enabledSites: This array contains all the websites that are written in  enabled field.
  • disabledSites: This array contains all the websites that are written in disabled field.

This process can be easily understood from the following code snippet:

let newCode = this.state.code;
let websiteData = newCode
 .split('::sites_enabled')[1]
 .split('::sites_disabled');
let enabledSites = websiteData[0].split(',');
let disabledSites = websiteData[1].split(',');

This data is stored inside an object.

Displaying the data:

The data fetched from the code view now has to be displayed in form of a table. This data is looks like this:

let configData = [
 {
   id: '1',
   name: 'website1.com',
   last: 'Jan 12, 2018 20:08 hrs',
   status: 1,
 },
 {
   id: '2',
   name: 'website2.com',
   last: 'Feb 19, 2018 13:00 hrs',
   status: 1,
 },
 {
   id: '3',
   name: 'website3.com',
   last: 'Mar 14, 2018 10:15 hrs',
   Status: 2,
 }
];

Status is 1 if the website is in enabled column and 2 if the website is in disabled column.
To display this data on the screen, we simply map through the data and display the rows of the table. The following code demonstrates it:

{configData.map((item, index) => {
 if (item.name) {
   return (
    <TableRow key={index}>
      <TableRowColumn style={{ fontSize: '16px' }}>
         {item.name}
     </TableRowColumn>
      <TableRowColumn style={{ fontSize: '16px' }}>
         {item.last}
     </TableRowColumn>
     <TableRowColumn>
       <SelectField
         floatingLabelText="Status"
         fullWidth={true}
         value={item.status}
       >
         <MenuItem value={1} primaryText="Enable" />
         <MenuItem value={2} primaryText="Disable" />
       </SelectField>
     </TableRowColumn>
    </TableRow>
   );
 }
})}

References:

Continue ReadingCode view in Configure tab of SUSI.AI Bot Wizard

Open Event Frontend – Settings Service

This blog illustrates how the settings of a particular user are obtained in the Open Event Frontend web app. To access the settings of the user a service has been created which fetches the settings from the endpoint provided by Open Event Server.

Let’s see why a special service was created for this.

Problem

In the first step of the event creation wizard, the user has the option to link Paypal or Stripe to accept payments. The option to accept payment through Paypal or Stripe was shown to the user without checking if it was enabled by the admin in his settings. To solve this problem, we needed to access the settings of the admin and check for the required conditions. But since queryRecord() returned a promise we had to re-render the page for the effect to show which resulted in this code:

canAcceptPayPal: computed('data.event.paymentCurrency', function() {     this.get('store').queryRecord('setting', {}) .then(setting => { this.set('canAcceptPayPal', (setting.paypalSandboxUsername || setting.paypalLiveUsername) && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).paypal); this.rerender(); });

This code was setting a computed property inside it and then re-rendering which is bad programming and can result in weird bugs.

Solution

The above problem was solved by creating a service for settings. This made sense as settings would be required at other places as well. The file was called settings.js and was placed in the services folder. Let me walk you through its code.

  • Extend the default Service provided by Ember.js and initialize store, session, authManager and _lastPromise.
import Service, { inject as service } from '@ember/service';
import { observer } from '@ember/object';

export default Service.extend({

 store       : service(),
 session     : service(),
 authManager : service(),

_lastPromise: Promise.resolve(),
  • The main method which fetches results from the server is called _loadSettings(). It is an async method. It queries setting from the server and then iterates through every attribute of the setting model and stores the corresponding value from the fetched result.
/**
* Load the settings from the API and set the attributes as properties on the service
*
* @return {Promise<void>}
* @private
*/
async _loadSettings() {
 const settingsModel = await this.get('store').queryRecord('setting', {});
 this.get('store').modelFor('setting').eachAttribute(attributeName => {
   this.set(attributeName, settingsModel.get(attributeName));
 });
},
  • The initialization of the settings service is handled by initialize(). This method returns a promise.
/**
* Initialize the settings service
* @return {*|Promise<void>}
*/
initialize() {
 const promise = this._loadSettings();
 this.set('_lastPromise', promise);
 return promise;
}
  • _authenticationObserver observes for changes in authentication changes and reloads the settings as required.
/**
* Reload settings when the authentication state changes.
*/
_authenticationObserver: observer('session.isAuthenticated', function() {
 this.get('_lastPromise')
   .then(() => this.set('_lastPromise', this._loadSettings()))
   .catch(() => this.set('_lastPromise', this._loadSettings()));
}),

The service we created can be directly used in the app to fetch the settings for the user. To solve the Paypal and Stripe payment problem described above, we use it as follows:

canAcceptPayPal: computed('data.event.paymentCurrency', 'settings.paypalSandboxUsername', 'settings.paypalLiveUsername', function() {
 return (this.get('settings.paypalSandboxUsername') || this.get('settings.paypalLiveUsername')) && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).paypal;
}),

canAcceptStripe: computed('data.event.paymentCurrency', 'settings.stripeClientId', function() {
 return this.get('settings.stripeClientId') && find(paymentCurrencies, ['code', this.get('data.event.paymentCurrency')]).stripe;
}),

Thus, there is no need to re-render the page and dangerously set the property inside its computed method.

References

Continue ReadingOpen Event Frontend – Settings Service