Disable editing for non-editable skills for non-admin users

As the Skills in SUSI Skill CMS are publicly editable, any user has the access to edit them. Hence, there needed to be a better control over who can edit the Skills in CMS. We needed to implement a feature to allow Admins and higher user roles to change the status of a Skill to non-editable. The subsequent implementation on CMS would require disabling editing for non-editable Skills for non-admin users. This blog post explains how this feature has been implemented in SUSI.AI.

Adding a boolean parameter ‘editable’ to the Skill metadata

We needed to add a boolean parameter in the Skill metadata for each Skill. The boolean parameter is ‘editable’. If its value is true, then it implies that editing should be allowed for that Skill. If it is set to false, then the Skill should not be editable for non-admin users. By default, its value has been set to true for all Skills. This is implemented as follows in the SusiSkill.java file:

    // in the getSkillMetadata() method
  skillMetadata.put("editable", getSkillEditStatus(model, group, language, skillname));

    // declaration of the getSkillEditStatus() method
    public static boolean getSkillEditStatus(String model, String group, String language, String skillname) {
        // skill status
        JsonTray skillStatus = DAO.skillStatus;
        if (skillStatus.has(model)) {
            JSONObject modelName = skillStatus.getJSONObject(model);
            if (modelName.has(group)) {
                JSONObject groupName = modelName.getJSONObject(group);
                if (groupName.has(language)) {
                    JSONObject languageName = groupName.getJSONObject(language);
                    if (languageName.has(skillname)) {
                        JSONObject skillName = languageName.getJSONObject(skillname);

                        if (skillName.has("editable")) {
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    }

 

Allowing Admin and higher user roles to change edit status of any Skill

This is facilitated by the endpoint ‘/cms/changeSkillStatus.json’. Its minimum base user role is set to Admin so that only Admins and higher user roles are able to change status of any Skill. A sample API call to this endpoint to change the edit status of any Skill to ‘false’ is as follows:

http://127.0.0.1:4000/cms/changeSkillStatus.json?model=general&group=Knowledge&language=en&skill=aboutsusi&editable=false&access_token=zdasIagg71NF9S2Wu060ZxrRdHeFAx

 

If we want to change the edit status of any Skill to ‘false’, then we need to add the Skill to the ‘skillStatus.json’ file. For this, we need to traverse inside the JSONObject in the ‘skillStatus.json’ file. We need to traverse inside the model, group and language as specified in the query parameters. This is done as follows:

   if(editable.equals("false")) {
       skill_status.put("editable", false);
   }

   JsonTray skillStatus = DAO.skillStatus;

   if (skillStatus.has(model_name)) {
        modelName = skillStatus.getJSONObject(model_name);
        if (modelName.has(group_name)) {
            groupName = modelName.getJSONObject(group_name);
            if (groupName.has(language_name)) {
                languageName = groupName.getJSONObject(language_name);

                if (languageName.has(skill_name)) {
                    skillName = languageName.getJSONObject(skill_name);

                    if(editable != null && editable.equals("false")) {
                        skillName.put("editable", false);
                    }
                    else if(editable != null && editable.equals("true")) {
                        skillName.remove("editable");
                    }

                    skillStatus.commit();
                    result.put("accepted", true);
                    result.put("message", "Skill status changed successfully.");
                    return new ServiceResponse(result);
                }
            }
        }
    }

 

If we want to change the edit status of any Skill to ‘true’, then we need to remove the Skill from the ‘skillStatus.json’ file. We also need to remove all the empty JSONObjects inside the ‘skillStatus.json’ file, if they are created in the process of removing Skills from it. This is done as follows:

   if (skillStatus.has(model_name)) {
        modelName = skillStatus.getJSONObject(model_name);
        if (modelName.has(group_name)) {
            groupName = modelName.getJSONObject(group_name);
            if (groupName.has(language_name)) {
                languageName = groupName.getJSONObject(language_name);
                if (languageName.has(skill_name)) {
                    skillName = languageName.getJSONObject(skill_name);
                    if(editable != null && editable.equals("true")) {
                        skillName.remove("editable");
                    }
                    if(skillName.length() == 0) {
                        languageName.remove(skill_name);
                        if(languageName.length() == 0) {
                            groupName.remove(language_name);
                            if(groupName.length() == 0) {
                                modelName.remove(group_name);
                                if(modelName.length() == 0) {
                                    skillStatus.remove(model_name);
                                }
                            }
                        }
                    }
                    skillStatus.commit();
                }
            }
        }
    }

 

Disabling editing for non-editable Skills for non-admin users on Skill CMS

For the Skills whose edit status has been set to ‘false’ by the Admins, we need to allow the non-admin users to only be able to view the code of the Skill, and not permit them to change the code and save the changes to the Skill. We need to display a message to the users about the possible reasons. All the code for displaying the message is put in an if() condition as follows:

   if (
      cookies.get('loggedIn') &&
      !this.state.editable &&
      !this.state.showAdmin
    )

 

This is how the Skill edit page for a non-editable Skill would look like for a non-admin user:

For an Admin user, this would look exactly same like an editable Skill page. Admin user would be able to edit and make changes to the Skill code and save the changes.

This is how editing of non-editable Skills have been disabled for non-admin users.

Resources

Continue ReadingDisable editing for non-editable skills for non-admin users

Giving users option to switch between All and Reviewed Only Skills on SUSI Skill CMS

There are a lot of Skills on SUSI Skill CMS. Any registered user has the access to creating his/her own Skills. Hence, we need to give the users an option on SUSI Skill CMS whether they want to see all the Skills, or only those Skills that have been tested thoroughly and have been approved by the Admin and higher user roles. This blog post explains how this feature has been implemented on the SUSI Skill CMS.

How is review status of any Skill changed on the server?

The API endpoint which allows Admin and higher user roles to change the review status of any Skill on the server is ‘/cms/changeSkillStatus.json’. It takes the following parameters:

  • model: Model of the Skill
  • group: Group of the Skill
  • language: Language of the Skill
  • skill: Skill name
  • reviewed: A boolean parameter which if true, signifies that the Skill has been approved.

Sample API call:

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

 

Fetching reviewed only Skills from the server

The ‘/cms/getSkillList.json’ endpoint has been modified to facilitate returning only the Skills whose review status is true. This is done by the following API call:

https://api.susi.ai/cms/getSkillList.json?reviewed=true

 

Creating checkbox to switch between All and Reviewed Only Skills

Checkbox is one of the many Material-UI components. Hence, we need to first import it before we can use it directly in our BrowseSkill component.

import Checkbox from 'material-ui/Checkbox';

 

In the constructor of the BrowseSkill class, we set states for two variables as follows:

constructor(props) {
  super(props);
    this.state = {
      // other state variables
      showSkills: '',
      showReviewedSkills: false,
    };
}

 

In the above code for the constructor, we have set two state variables. ‘showSkills’ is a string which can either be an empty string, or ‘&reviewed=true’. We want to append this string to the ‘/cms/getSkillList.json’ API call because it would determine whether we want to fetch All Skills or reviewed only Skills. The second variable ‘showReviewedSkills’ is a boolean used to keep record of the current state of the page. If it is true, then it means that currently, only the reviewed Skills are being displayed on the CMS site.

Implementation of the checkbox

This is how the Checkbox has been implemented for the purpose of switching between All and Reviewed Only Skills:

 <Checkbox
    label="Show Only Reviewed Skills"
    labelPosition="right"
    className="select"
    checked={this.state.showReviewedSkills}
    labelStyle={{ fontSize: '14px' }}
    iconStyle={{ left: '4px' }}
    style={{
      width: '256px',
      paddingLeft: '8px',
      top: '3px',
    }}
    onCheck={this.handleShowSkills}
  />

 

As can be seen from the above code, the initial state of the checkbox is unchecked as initially, the value of the state variable ‘showReviewedSkills’ is set to false in the constructor. This means that initially all Skills will be shown to the user. On clicking on the checkbox, handleShowSkills() function is called. Its implementation is as follows:

  handleShowSkills = () => {
    let value = !this.state.showReviewedSkills;
    let showSkills = value ? '&reviewed=true' : '';
    this.setState(
      {
        showReviewedSkills: value,
        showSkills: showSkills,
      },
      function() {
        this.loadCards();
      },
    );
  };

 

In the handleShowSkills() function, firstly we store the current value of the state variable ‘showReviewedSkills’. The value of ‘showSkills’ string is determined according to the value of ‘showReviewedSkills’. Then the states of both these variables are updated in the setState() function. Lastly, loadCards() function is called.

In the loadCards() function, we append the value of the state variable ‘showSkills’ to the AJAX call to the ‘/cms/getSkillList.json’ endpoint. The URL used for the API call is as follows:

https://api.susi.ai/cms/getSkillList.json?group=' +
  this.props.routeValue +
  '&language=' +
  this.state.languageValue +
  this.state.filter +
  this.state.showSkills;

 

This is how the implementation of the feature to give users an option to switch between All and Reviewed Only Skills has been done.

Resources

Continue ReadingGiving users option to switch between All and Reviewed Only Skills on SUSI Skill CMS

Implementing a device wise usage section on the skill page

SUSI Skill CMS showcases all the skills on the index page as skill cards and users can visit any skill page for any skill by clicking on any of these cards, skill pages for each skill hold some interesting metrics like rating, usage data, country wise usage data etc. But since SUSI runs on different devices so we need something to distribute and showcase how a skill is performing on each device so we implemented a pie chart for visualization of device wise usage data.

About the API

An API is developed at the server so from the client we call this API to fetch data from the server and plug this data into the chart we wish to render.

Endpoint :

/cms/getDeviceWiseSkillUsage.json

 

Parameters :

  • model
  • group
  • language
  • skill

Sample API call :

/cms/getDeviceWiseSkillUsage.json?model=general&group=Knowledge&language=en&skill=ceo

 

Response

{
 "skill_usage": [
   {
     "count": 3,
     "device_type": "Others"
   },
   {
     "count": 39,
     "device_type": "Android"
   },
   {
     "count": 1,
     "device_type": "Web Client"
   }
 ],
 "session": {"identity": {
   "type": "host",
   "name": "162.158.166.37_35449f1b",
   "anonymous": true
 }},
 "skill_name": "news",
 "accepted": true,
 "message": "Device wise skill usage fetched"
}

Fetching the data for the chart

Setting the URL to fetch data from, this URL will be used to make the AJAX call.

let deviceUsageUrl = `${urls.API_URL}/cms/getSkillsByAuthor.json?author_email=${cookies.get('emailId')}`;
deviceUsageUrl = deviceUsageUrl + '?model=' + modelValue + '&group=' + this.groupValue + '&language=' + this.languageValue + '&skill=' + this.name;

 

Make an ajax call to extract data from the response and call a function which saves the data to the application state, this data will later be used to render our chart we wish to render.

$.ajax({
 url: deviceUsageUrl,
 ...
 success: function(data) {
   if (data.skill_usage) {
     self.saveDeviceUsageData(data.skill_usage);
   }
 },
 error: function(e) {
   self.saveDeviceUsageData();
 },
});

 

Set the application state with the received data which the pie chart component will use as it’s data source.

saveDeviceUsageData = (device_usage_data = []) => {
 this.setState({
   device_usage_data,
 });
};

Implementing the UI

We already have a card component for device usage section so we append our device wise usage section to this already present card. We fetch the data in the skillListing component and pass that data as props to the skill usage component so using data from the received props we render our pie chart.

Importing the needed components from recharts library.

import { Tooltip, Legend, PieChart, Pie, Sector, Cell } from 'recharts';

 

Rendering the Piechart component with appropriate props, the data props is the most important which is taken from the application state which we saved earlier.

<PieChart width={600} height={350}>
 <Pie
   data={this.props.device_usage_data}
   nameKey="device_type"
   dataKey="count"
   onMouseEnter={this.onPieEnter}
   ...
 >
   ...
 </Pie>
 <Legend wrapperStyle={{ position: 'relative' }} />
</PieChart>

 

Configuring color for each Cell in the pie so it looks more interactive and we have distinguished colors for all devices.

{this.props.device_usage_data.map((entry, index) => (
 <Cell
   key={index}
   fill={
     [
       '#0088FE',
       '#00C49F',
       '#FFBB28',
       '#FF8042',
       '#EA4335',
     ][index % 5]
   }
 />
))}

 

Rendering the Pie only when data is available in props so we don’t end up rendering a blank chart which obviously won’t look good.

{
 this.props.device_usage_data !== [] ? (
   ...
 ): ''
}

 

Resources

  • Swizec Teller, Rendering a pie chart using react and d3, URL
  • Pie chart example from recharts, URL
Continue ReadingImplementing a device wise usage section on the skill page

How api.susi.ai responds to a query and send response

A direct way to access raw data for a query is https://api.susi.ai. It is an API of susi server which sends responses to a query by a user in form of a JSON (JavaScript Object Notation) object (more on JSON here). This JSON object is the raw form of any response to a query which is a bunch of key (attribute) value pair. This data is then send from the server to various APIs like chat.susi.ai, susi bots, android and ios apps of SUSI.AI.

Whenever a user in using an API, for example chat.susi.ai, the user send a query to the API. This query is then sent by the API as a request to the susi server. The server then process the request and sends the answer to the query in form of json data as a response to the request. This request is then used by the API to display the answer after applying stylings to the json data.

Continue ReadingHow api.susi.ai responds to a query and send response

Displaying Skills Feedback on SUSI iOS

SUSI allows the user to rate the SUSI Skills with the five-star rating system. SUSI offer a good feedback system where the user can post feedback to any skill by using iOS, Android, and Web clients. In Skill Detail, there is a skill feedback text field where the user can write feedback about SUSI Skill. We display the users posted feedbacks on Skill Detail screen. In this post, we will see how the displaying skills feedback feature implemented on SUSI iOS.

Implementation –

We are displaying three feedback on Skill Detail screen, to see all feedback, there is a “See All Review” option, by clicking user is directed to a new screen where he/she can see all feedback related to particular skill.

We use the endpoint below for getting skill feedback from server side –

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

With the following params:

  • Model
  • Group
  • Language
  • Skill Name

The API endpoint above return the all the feedback array related to particular susi skill. We store feedbacks in an array of Feedback object, which holds three value:

    • Feedback String – Feedback string posted by the user
    • Email – Email address of feedback poster user
    • Time Stamp – Time of posting feedback
class Feedback: NSObject {
var feedbackString: String = ""
var email: String = ""
var timeStamp: String = ""
...
}

To display feedbacks, we are using UITableView with two prototype cells, one for feedbacks and one for “See All Review” option.

There can be different cases eg. when the total number of feedback for skill is less than three or three. When the feedback count is three or less than three, there is no need to show “See All Review” option. Also, tableView height is different for different feedback count. For varying tableView height, we have created an outlet for tableView height constraints and vary accordingly.

@IBOutlet weak var feedbackTableHeighConstraint: NSLayoutConstraint!

Now, let’s see how the number of cells, height for cells and different cells presented according to feedback count with UITableViewDelegate and UITableViewDataSource methods.

Handling number of tableView rows –

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let feedbacks = feedbacks, feedbacks.count > 0 else {
feedbackTableHeighConstraint.constant = 0.0
return 0
}
if feedbacks.count < 4 {
feedbackTableHeighConstraint.constant = CGFloat(72 * feedbacks.count)
return feedbacks.count
} else {
feedbackTableHeighConstraint.constant = 260.0
return 4
}
}

Where feedbacks is the array of Feedback object which holds the feedbacks we are getting from the server side for a skill.

var feedbacks: [Feedback]?

In the above method, we see that how we are handling the number of cells case. Now let’s see how to handle which cells to be present on basis of the number of cells case –

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let feedbacks = feedbacks, feedbacks.count > 0 else {
feedbackTableHeighConstraint.constant = 0.0
return UITableViewCell()
}
if feedbacks.count < 4 {
if let cell = tableView.dequeueReusableCell(withIdentifier: "feedbackDisplayCell", for: indexPath) as? FeedbackDisplayCell {
cell.feedback = feedbacks[indexPath.row - Int(indexPath.row/2)]
return cell
}
} else if feedbacks.count > 3 {
if indexPath.row == 3 {
let cell = tableView.dequeueReusableCell(withIdentifier: "allFeedbackCell", for: indexPath)
return cell
} else {
if let cell = tableView.dequeueReusableCell(withIdentifier: "feedbackDisplayCell", for: indexPath) as? FeedbackDisplayCell {
cell.feedback = feedbacks[indexPath.row]
return cell
}
}
}
return UITableViewCell()
}

If the number of feedbacks is greater than three than we provide “See All Review” option to the user to see all the feedback related to skill. We are displaying all feedbacks using UITableViewController. When the user clicks the “See All Review” option, we pass the feedbacks (Array of all the feedback) to new UITableViewController. By passing feedbacks, we are reducing one network call.

let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let allFeedbackVC = storyboard.instantiateViewController(withIdentifier: "AllFeedbackController") as? AllFeedbackViewController {
allFeedbackVC.feedbacks = self.feedbacks
let nvc = AppNavigationController(rootViewController: allFeedbackVC)
self.present(nvc, animated: true, completion: nil)
}

On all skill feedback screen, we are displaying the full review. For the different size of text, we are setting the different size of cell size by using the method below:

if let feedbacks = feedbacks {
let estimatedLabelHeight = UILabel().heightForLabel(text: feedbacks[indexPath.row].feedbackString, font: UIFont.systemFont(ofSize: 14.0), width: 250.0)
return 64 + estimatedLabelHeight
} else {
return 80
}

 

Final Output –

Resources –

  1. SUSI API Link: https://api.susi.ai/
  2. SUSI iOS Link: https://github.com/fossasia/susi_iOS
  3. Apple’s Documentation on UITableViewDelegate: https://developer.apple.com/documentation/uikit/uitableviewdelegate?changes=_6
  4. Apple’s Documentation on UITableViewDataSource: https://developer.apple.com/documentation/uikit/uitableviewdatasource
Continue ReadingDisplaying Skills Feedback on SUSI iOS

Fetching Skills using the ID of a user

The Dashboard on the SUSI Skill CMS is the page where all user specific Skill information is being displayed. It also has a section called “My Skills” which displays the Skills created by the user himself. The data for the “My Skills” section has to be fetched from the Skill metadata of each Skill. Hence, this blog post explains how the Skills created by an author are fetched using an API which takes author’s email as the only parameter.

Why is such an API required?

The task of displaying the “My Skills” section cannot be done entirely on the Client side with the use of existing APIs, as that would result in a lot of AJAX calls to the Server, because for each Skill, there would have to be an AJAX call for each user. Hence, we had to create an endpoint which could directly return the list of Skills created by a user using his ID. This would prevent multiple AJAX calls to the Server.

The endpoint used is:

"https://api.susi.ai/cms/getSkillsByAuthor.json?author_email="+author_email

 

Here, author_email is the email of the author who published the particular Skill, which is fetched from the Skill metadata object which looks like this:

"Antonyms": {
      // skill metadata
      "author_email": akjn11@gmail.com,
      // skill metadata
    }

Working of the API

The code to fetch the Skills created by an author is described in the GetSkillsByAuthor.java file. This API accepts either the author’s name, or the author’s email as the parameter. In our

We get all folders within a folder using the following method:

private void listFoldersForFolder(final File folder, ArrayList<String> fileList) {
        File[] filesInFolder = folder.listFiles();
        if (filesInFolder != null) {
            Arrays.stream(filesInFolder).filter(fileEntry -> fileEntry.isDirectory() && !fileEntry.getName().startsWith(".")).forEach(fileEntry->fileList.add(fileEntry.getName() + ""));
        }
    }

 

We use this method to get list of folders of all the group names for all the different Skills, and store them in a variable ‘folderList’ as per the following code:

File allGroup = new File(String.valueOf(model));
ArrayList<String> folderList = new ArrayList<String>();
listFoldersForFolder(allGroup, folderList);

 

For every folder corresponding to a group name, we traverse inside it and then further traverse inside the language folders to reach the place where all the Skill files are stored.

This is the code for the method to get all files within a folder:

private void listFilesForFolder(final File folder, ArrayList<String> fileList) {
    File[] filesInFolder = folder.listFiles();
    if (filesInFolder != null) {
        Arrays.stream(filesInFolder).filter(fileEntry -> !fileEntry.isDirectory() && !fileEntry.getName().startsWith(".")).forEach(fileEntry->fileList.add(fileEntry.getName() + ""));
    }
}

 

We need to get all Skill files inside the ‘languages’ folder, which itself is in ‘group’ folder. This is the code required to implement the same:

for (String temp_group_name : folderList){
        File group = new File(model, temp_group_name);
        File language = new File(group, language_name);
        ArrayList<String> fileList = new ArrayList<String>();
        listFilesForFolder(language, fileList);

        for (String skill_name : fileList) {
               // get skill metadata of each skill iteratively and put it in result JSONObject accordingly
            }
         }
    }

 

This is how the Skill metadata is fetched from the getSkillMetadata() method of SusiSkill class:

skill_name = skill_name.replace(".txt", "");
            JSONObject skillMetadata = SusiSkill.getSkillMetadata(model_name, temp_group_name, language_name, skill_name);

            if(skillMetadata.get("author_email").equals(author_email)) {
                skillMetadata.put("skill_name", skill_name);
                authorSkills.put(skillMetadata);

 

getSkillMetadata() method takes model of the Skill, group of the Skill, language of the Skill, Skill name and also duration (for usage statistical purposes) as the method parameters, and returns the metadata for the Skill in the format as shown below:

   "Antonyms": {
      "model": "general",
      "group": "Knowledge",
      "language": "en",
      "developer_privacy_policy": null,
      "descriptions": "A skill that returns the antonyms for a word",
      "image": "images/antonyms.png",
      "author": "akshat jain",
      "author_url": "https://github.com/Akshat-Jain",
      "author_email": akjn11@gmail.com,
      "skill_name": "Antonyms",
      "protected": false,
      "terms_of_use": null,
      "dynamic_content": true,
      "examples": ["Antonym of happy"],
      "skill_rating": {
        "negative": "0",
        "bookmark_count": 0,
        "positive": "0",
        "stars": {
          "one_star": 0,
          "four_star": 0,
          "five_star": 0,
          "total_star": 0,
          "three_star": 0,
          "avg_star": 0,
          "two_star": 0
        },
        "feedback_count": 0
      },
      "usage_count": 0,
      "skill_tag": "Antonyms",
      "creationTime": "2018-05-06T03:05:22Z",
      "lastAccessTime": "2018-06-23T18:48:36Z",
      "lastModifiedTime": "2018-05-06T03:05:22Z"
    }

 

This is how the Skills created by an author are fetched using this API. The JSON response of this endpoint shows the metadata of all Skills which have ‘author_email’ value the same as that given in the endpoint’s query parameter.

Resources

Continue ReadingFetching Skills using the ID of a user

Implementing API to Fetch all Ratings by a User on Different Skills on SUSI.AI Web Client

SUSI Skill CMS allows the users to rate any Skill on a scale of 1 to 5. The user can also provide a feedback to any Skill. This paves the path to implementing a Dashboard, which has all the analytic data of the user. Hence, an API needed to be implemented which could return the ratings done by a particular user on all different Skills.

How are servlets implemented in SUSI.AI?

All servlets in SUSI extend AbstractAPIHandler class and implement APIHandler. All servlets have 4 methods, which we overwrite depending on what we want the servlet to do. They are as follows :

    @Override
    public String getAPIPath() {
        return null;
    }

    @Override
    public BaseUserRole getMinimalBaseUserRole() {
        return null;
    }

    @Override
    public JSONObject getDefaultPermissions(BaseUserRole baseUserRole) {
        return null;
    }

    @Override
    public ServiceResponse serviceImpl(Query post, HttpServletResponse response, Authorization rights, JsonObjectWithDefault permissions) throws APIException {
        return null;
    }

 

How these 4 methods work together?

  • First method is getAPIPath(). It returns the endpoint of the servlet.
  • The second method is getMinimalBaseUserRole(). It returns the minimum privilege level required to access the endpoint.
  • The third method is getDefaultPermissions(). It gets the Default Permissions of a UserRole in SUSI Server. Different UserRoles have different permissions defined in SUSI Server.
  • Whenever the endpoint defined in the getAPIPath() method is called properly, it responds with whatever is defined in the fourth method, which is serviceImpl().

Implementing a servlet to fetch all ratings by a user on different Skills

The task of this servlet is to fetch all the ratings done by a user on all the different Skills, so that this fetched data could be used later on for implementation of various user specific features like Dashboard page. This is the implementation of the 4 methods of this servlet:

    @Override
    public UserRole getMinimalUserRole() {
        return UserRole.USER;
    }

    @Override
    public JSONObject getDefaultPermissions(UserRole baseUserRole) {
        return null;
    }

    @Override
    public String getAPIPath() {
        return "/cms/getProfileDetails.json";
    }

    @Override
    public ServiceResp onse serviceImpl(Query query, HttpServletResponse response, Authorization authorization, final JsonObjectWithDefault permissions) throws APIException {

    JsonTray fiveStarSkillRating = DAO.fiveStarSkillRating;
    // JSONObject and JsonArray Declarations

    // Checking if access_token has been given and is valid
    if (authorization.getIdentity() == null) {
        throw new APIException(400, "Specified user data not found, ensure you are logged in");
    } 


    // Fetching email of the user from access token and storing it in a string
    String email = authorization.getIdentity().getName();


    // Iterating over the fiveStarSkillRating JsonTray by extracting keys at every level and accessing their child objects through the extracted keys
    for(String model_name : fiveStarSkillRating.keys())
    {
        // Storing the list of group names and iterating over them
        for(String group_name : groupnameKeysList)
        {
            // Storing the list of language names and iterating over them
            for(String language_name : languagenameKeysList)
            {
                // Storing the list of skill names and iterating over them
                for(String skill_name : skillnamesKeysList)
                {
                    skillnameArray = languageObject.getJSONArray(skill_name);
                    // Iterating over the different Skill JSONObjects
                    for(int i=0; i<skillnameArray.length(); i++) {
                        String jsonEmail = skillnameArray.getJSONObject(i).get("email").toString();
                        if(jsonEmail.equals(email)) {
                        // If the Skill has been rated by the required author, then put the Skill JSONObject into the result array
                        }
                    }
                }
            }
        }
    }

    if(result.length()==0) {
        // Handling case when user hasn’t rated any Skill yet
        result.put("accepted", false);
        result.put("message", "User has not rated any Skills yet.");
        return new ServiceResponse(result);
    }
    result.put("accepted", true);
    result.put("message", "User ratings fetched.");
    return new ServiceResponse(result);
    }

 

As it can be seen from the above code, the endpoint for this servlet is /cms/getProfileDetails.json and it requires 1 parameter – the access token of the user.

As the main task of this servlet is user specific, and should only be accessible to the particular user, hence we returned UserRole as USER in the getMinimalUserRole() method.

In the serviceImpl() method, we iterate over the fiveStarSkillRating.json JsonTray and keep on extracting keys and accessing the corresponding JSONObjects until we reach the lowermost layer, where all the Skill names are listed. Iterating over the JSONObjects corresponding to each Skill, we check the email in the Skill JSONObject to identify the user. If the email present in the Skill JSONObject matches with the email corresponding to the access token provided as a query parameter, we extract rating and the timestamp for the Skill and put it in a JSONArray. Then, finally we put the JSONArray into our resulting JSONObject.

This is how we can fetch all the ratings by a user on all the different Skills.

Resources

 

Continue ReadingImplementing API to Fetch all Ratings by a User on Different Skills on SUSI.AI Web Client

Implementing API to facilitate changing review status of a Skill

As any registered user can make Skills in SUSI Skill CMS, sometimes the Skill made is not up to the mark to be displayed on the CMS site. There needs to be a feature implemented where the Admin and higher user roles can review a Skill and approve it accordingly. Then only the approved Skills should be displayed on the Skill CMS. This feature required implementation of an API to allow Admin and higher user roles to change the review status of a Skill. This blog post explains how such an API has been implemented.

Implementing a servlet to allow changing review status of a Skill

The basic task of the servlet is to allow Admin and higher user roles to allow changing the review status of a Skill. The review status of a Skill can either be ‘true’, indicating that the Skill has been approved by the Admin, or ‘false’, which would indicate that the Skill still needs some improvements before it is actually displayed on the SUSI Skill CMS.

Here is the implementation of the API:

  1. The API should be usable to only the users who have a user role Admin or higher. Only those with minimum Admin rights should be allowed to control what Skills are displayed on the CMS site. This is implemented as follows:
   @Override
    public UserRole getMinimalUserRole() {
        return UserRole.ADMIN;
    }

 

  1. The endpoint for the API is ‘/cms/changeSkillStatus.json’. This is implemented as follows:
   @Override
    public String getAPIPath() {
        return "/cms/changeSkillStatus.json";
    }

 

  1. The main method of the servlet is the serviceImpl() method. This is where the actual code goes which will be executed each time the API is called. This is implemented as follows:

    @Override
    public ServiceResponse serviceImpl(Query call, HttpServletResponse response, Authorization authorization, JsonObjectWithDefault permissions) throws APIException {

       // Store the query parameters in variables

        if(skill_name == null || !(reviewed.equals("true") || reviewed.equals("false"))) {
            throw new APIException(400, "Bad service call, missing arguments.");
        }

        JSONObject result = new JSONObject();
        JsonTray skillStatus = DAO.skillStatus;
        JSONObject modelName = new JSONObject();
        JSONObject groupName = new JSONObject();
        JSONObject languageName = new JSONObject();
        JSONObject skillName = new JSONObject();

        if(reviewed.equals("true")) {
            JSONObject reviewStatus = new JSONObject();
            reviewStatus.put("reviewed", true);
                // traverse through the skillStatus.json file down to the Skill name and put review status ‘true’ in it
                skillName.put("reviewed", true);
                result.put("accepted", true);
                result.put("message", "Skill review status changed successfully.");
                return new ServiceResponse(result);
            }


            // deleting skill name from skillStatus.json file if it’s review status is being changed to ‘false’
            else {
                if (skillStatus.has(model_name)) {
                    modelName = skillStatus.getJSONObject(model_name);
                    if (modelName.has(group_name)) {
                        groupName = modelName.getJSONObject(group_name);
                        if (groupName.has(language_name)) {
                          languageName = groupName.getJSONObject(language_name);
                            if (languageName.has(skill_name)) {
                                languageName.remove(skill_name);
                                    skillStatus.remove(model_name);
                                }
                                skillStatus.commit();
                            }
                        }
                    }
                }
                result.put("accepted", true);
                result.put("message", "Skill review status changed successfully.");
                return new ServiceResponse(result);
            }
        }

 

The list of reviewed Skills is being stored in the ‘skillStatus.json’ file. So we need to traverse through that file and store the review status of the Skill as required while making the API call.

The API takes 5 parameters:

  • Model of the Skill
  • Group of the Skill
  • Language of the Skill
  • Skill name
  • Review status to be set for the Skill – true or false

On making the API call, if the value of the query parameter ‘reviewed’ is ‘true’, then the Skill name along with its review status is being appended in the ‘skillStatus.json’ file. This is done by traversing through the file using the specified model, group and language info of the Skill.

However, if the value of the query parameter ‘reviewed’ is ‘false’, then we need to check if the Skill is already there in the ‘skillStatus.json’ file. If it is already there, then we remove its entry from the file. If it isn’t in the file already, then it’s review status is already ‘false’. We also need to commit this change to the JsonTray for it to get reflected in the Server.

This is how an API has been implemented which would allow Admin and higher user roles to change the review status of any Skill, which would then facilitate showing only the approved Skills on the CMS site.

  1. Resources

     

Continue ReadingImplementing API to facilitate changing review status of a Skill

SUSI AI 5 Star Skill Rating System

For making a system more reliable and robust, continuous evaluation is quite important. So is in case of SUSI AI. User feedback is important to improve SUSI skills and create new ones. Previously we had only thumbs up / thumbs down as a feedback method, from the susi chat client. But now a 5 star rating system has been added to the SUSI Skill CMS so that users can rate a skill there. Before the implementation of API  let’s look how data is stored in SUSI AI Susi_server uses DAO in which skill rating is stored as JSONTray.

The server side implementation

A new java class has been created for the API, FiveStarRateSkillService.java.

public class FiveStarRateSkillService extends AbstractAPIHandler implements APIHandler {
    private static final long serialVersionUID =7947060716231250102L;
    @Override
    public BaseUserRole getMinimalBaseUserRole() {
        return BaseUserRole.USER;
    }
    @Override
    public JSONObject getDefaultPermissions(BaseUserRole baseUserRole) {
        return null;
    }
    @Override
    public String getAPIPath() {
        return "/cms/rateSkill.json";
    }
...
}

The getMinimalBaseRole method tells the minimum User role required to access this servlet it can also be ADMIN, USER or ANONYMOUS. In our case it is USER. A user needs to be logged in to rate a skill on a scale of 1-5 stars.  The API runs at “/cms/fiveStarRateSkill.json” endpoint.

Next, create serviceImpl method in the above class to handle the request from the client and respond to it.

1. Fetch the required query parameters and store them in variables. They include skill model, group, language, skill name and starts that the user has given in the rating.

String skill_name = call.get("skill", null);
String skill_stars = call.get("stars", null);

2. Then check if the skill exists. If not them throw an exception. Otherwise, increment the count of the corresponding rating. The rating object has keys as one_star, two_star, three_star, four_star and five_star that has the count of that star rating.       

if (skill_stars.equals("1")) {
    skillName.put("one_star", skillName.getInt("one_star") + 1 + "");
}
else if (skill_stars.equals("2")) {
    skillName.put("two_star", skillName.getInt("two_star") + 1 + "");
}
else if (skill_stars.equals("3")) {
    skillName.put("three_star", skillName.getInt("three_star") + 1 + "");
}
else if (skill_stars.equals("4")) {
    skillName.put("four_star", skillName.getInt("four_star") + 1 + "");
}
else if (skill_stars.equals("5")) {
    skillName.put("five_star", skillName.getInt("five_star") + 1 + "");
}

3. Re-calculate the total rating done on that skill and its average rating and update the object. If the skill has not been already rated then create a new rating object and initialize it with the 0 star counts.

public JSONObject createRatingObject(String skill_stars) {
        JSONObject skillName = new JSONObject();
        JSONObject skillStars = new JSONObject();

        skillStars.put("one_star", 0);
        skillStars.put("two_star", 0);
        skillStars.put("three_star", 0);
        skillStars.put("four_star", 0);
        skillStars.put("five_star", 0);
        skillStars.put("avg_star", 0);
        skillStars.put("total_star", 0);

        skillName.put("stars", skillStars);
}

The complete FiveStarRateSkillService.java is available here : –

https://github.com/fossasia/susi_server/blob/development/src/ai/susi/server/api/cms/FiveStarRateSkillService.java

Rating a skill

Sample endpoint

https://api.susi.ai/cms/fiveStarRateSkill.json?model=general&group=Knowledge&language=en&skill=aboutsusi&stars=3&callback=p&_=1526813916145

This gives 3 star rating to the “aboutsusi” skill.

Parameters

  • Model
  • Group
  • Language
  • Skill
  • Stars

Response

{
  "ratings": {
    "one_star": 0,
    "four_star": 0,
    "five_star": 1,
    "total_star": 1,
    "three_star": 0,
    "avg_star": 5,
    "two_star": 0
  },
  "session": {"identity": {
    "type": "email",
    "name": "1anuppanwar@gmail.com",
    "anonymous": false
  }},
  "accepted": true,
  "message": "Skill ratings updated"
}

Getting the stats of Skill Ratings

Sample endpoint

http://127.0.0.1:4000/cms/getSkillRating.json?model=general&group=Knowledge&language=en&skill=aboutsusi&callback=p&_=1526813916145

This fetches the current ratings of the “aboutsusi” skill.

Parameters

  • Model
  • Group
  • Language
  • Skill

Response

{
    "session": {
        "identity": {
            "type": "host",
            "name": "172.68.144.159_81c88a10",
            "anonymous": true
        }
    },
    "skill_name": "aboutsusi",
    "accepted": true,
    "message": "Skill ratings fetched",
    "skill_rating": {
        "negative": "0",
        "positive": "0",
        "stars": {
            "one_star": 0,
            "four_star": 2,
            "five_star": 1,
            "total_star": 4,
            "three_star": 1,
            "avg_star": 4,
            "two_star": 0
        },
        "feedback_count": 3
    }
}

Conclusion

So this 5 star rating system will help in improving the SUSI skills. Also, it will help in making better decisions when we have multiple similar skills and we have to choose one to respond to the user query.

References

Continue ReadingSUSI AI 5 Star Skill Rating System

Post feedback for SUSI Skills in SUSI iOS

SUSI iOS, web and Android clients allow the user to rate the SUSI Skills in a 5-star rating system. Users can write about how much particular skill is helpful for them or if improvements are needed. Users can rate skills from one to five star as well. Here we will see how to submit feedback for SUSI skills and how it is implemented on SUSI iOS.

How to submit feedback –

  1. Go to Skill Listing Screen < Skill Detail Screen
  2. Scroll to the feedback section
  3. Write feedback about SUSI skill
  4. Click on POST button to post the skill feedback

An anonymous user can not submit skill feedback. You must have to logged-in in order to post skill feedback. If you are not logged-in and click POST button to post skill feedback, an alert is presented with Login option, by clicking Login, the user is directed to Login screen where the user can log in and later can post skill feedback.

Implementation of posting skill feedback –

Google’s Material textfield is used for skill feedback text field. We have assigned TextField class from Material target to skill feedback text field to make it very interactive and give better user experience.

Skill feedback text field in the normal state –

Skill feedback text field in the active state –

When the user clicks POST after writing skill feedback, we check if the user is logged-in or not.

if let delegate = UIApplication.shared.delegate as? AppDelegate, let user = delegate.currentUser {
...
}

We have saved the logged-in user globally using AppDelegate shared method during login and using it here. The AppDelegate is sort of like the entry point for the application. It implements UIApplicationDelegate and contains methods that are called when application launches, when is going to the background (i.e. when the user hit the home key), when it’s opened back up, and more. The AppDelegate object is stored as a property on the UIApplication class and is accessible from anywhere in swift classes.

Case 1: If the user is not logged-in, we show a popup to the user with the login option

By clicking Login, the user is directed to Login screen where the user can log in and later can post skill feedback.

Case 2: If the user is already logged-in, we use the endpoint below for posting skill feedback –

http://api.susi.ai/cms/feedbackSkill.json

ModelWith following parameters –

  • Group
  • Skill
  • Feedback
  • Access token
Client.sharedInstance.postSkillFeedback(postFeedbackParam) { (feedback, success, responseMessage) in
DispatchQueue.main.async {
if success {
self.skillFeedbackTextField.text = ""
self.skillFeedbackTextField.resignFirstResponder()
self.view.makeToast(responseMessage)
} else {
self.view.makeToast(responseMessage)
}
}
}

In return response, we get feedback posted by the user –

{
feedback: "Helpful",
session:
{
...
},
accepted: true,
message: "Skill feedback updated"
}

 

Resources –

  1. Material Design Guidelines for iOS
  2. Apple’s documentation on UIApplicationDelegate API
  3. Apple’s documentation on UIApplication API
  4. ChrisRisner’s article on Singletons and AppDelegate
Continue ReadingPost feedback for SUSI Skills in SUSI iOS