For Developers

Introduction

Here we will cover some tips & tricks to help you write your own routines.

Is it complicated?

Not at all. 🙂

Routines are written as scripts in python or node.js and managed in the Solvemate Web App. A routine expects one or more input parameters, which the customer can enter. The script then can use these inputs to request APIs from third-party systems.

Typically routines needs to make only 1 to 3 calls to API endpoints and they are ready to automate your service flows!

Who can code routines?

Anyone who can answer yes to the following:

  • I have basic experience with pyhton or node.js
  • I know what HTTP methods, especially GET and POST are
  • I know what status codes like 200, 201 and 404 mean
  • I know how to work with JSON objects

However, if the routine needs to handle complex business logic or the API is not REST but e.g. SOAP, the routine code will be the best handled by an experienced developer.

Environment

Available coding environments

Available routine runtimes are:

  • Python 3.6
  • Node.js 12.x

Routine runtime environments can be set in the Solvemate Web App in the “Edit” mode of the respective routine component.

Choose runtime

Available libraries

Routines are executed in an isolated environment and all standard libraries are available.

In addition, all newly created routine components (created after September 2020) have more libraries available.

Currently available modules:

  • requests - elegant and simple HTTP library for Python, built for human beings,
  • pytz - this library allows accurate and cross platform timezone calculations.
  • Coming soon!
Approach your Success Manager at Solvemate if you need further packages installed.

To use libraries in the routine code simply import them:

import requests

def handler(event, context):
   response = requests.get('https://solvemate.com')
   return response.status_code
// Coming soon!

Debugging

Depending on the complexity of the routine, it can be easier to develop and debug the code locally in an IDE by your preference.

To simulate user input in the form fields simply add a code that collects user input at the end of the routine.py file and pass it to the handler function as the event parameter. You can specify default values after or for faster development.

def handler(event, context):
   order_id = event['order_id']
   user_email = event['email']
   return user_email

# "event" simulates user input from the Widget.
if __name__ == "__main__":
   order_id = input("Enter order_id:") or 'RX123456'
   email = input("Enter your email:") or 'example@solvemate.com'
   event = {'order_id': order_id, 'email': email}
   print(handler(event, {}))
// Coming soon!

You will not be able to simulate use of Secrets locally. In case you develop routine locally and will use Secrets in the Web App the code will need to be slightly adjusted once pasted in the Solvemate Web App.

Testing

There are few things to note when it comes to testing routine code from within the Solvemate Web App:

  • The input fields in Test Routine section in the routine “Edit” mode will not validate input,
  • To test user input validation e.g. RegEx use the Widget,
  • Routine code in the Web App can not be edited simultaneously from 2 or more accounts,
  • From within Web App (either Test routine section or Widget) you will always be testing the latest changes
  • To test latest changes from another page (e.g. your website or expert page) always Publish the bot

Routine basics

The routine result visible in the Widget to the customer is the returned value of handler(). This function must always return a result.

Make requests to API

GET

import requests

token_url = 'my.api.endpoint.com/get_token'
order_url = 'my.api.endpoint.com/orders/{order_id}'


def get_token():
   ...
   ...
   return token

def handler(event, context):
   order_id = event['order_id']
   
   token = get_token()
   headers = {'Authorization': token}

   url = order_url.format(order_id=order_id)

   # GET request with extra headers
   response = requests.get(url, headers=headers)
   print(response.status_code)
   print(response.headers['content-type'])
   print(response.text)
   print(response.json())
   data = response.json()
   return data['best_bot']['ever']

// Coming soon!

POST

import requests

username = 'my-username'
password_cipher = b64decode(os.environ['password'])
password = boto3.client('kms').decrypt(CiphertextBlob=password_cipher)['Plaintext'].decode('utf-8')

token_url = 'my.api.endpoint.com/get_token'


def get_token():
    payload = {
        'username': username,
        'pw': password
    }

    # POST with payload    
    response = requests.post(token_url, data=payload)

    return response['access_token']

def handler(event, context):
    order_id = event['order_id']
    
    token = get_token()
    ..
    return "Hello!"

// Coming soon!

To make the routines end-user friendly, editable by non-developer and readable you might want to:

  • Catch the errors and return prepared texts to the enduser
  • Define all texts that the routine will return before the code in a structured way so that it can easily be edited by Chatbot manager without needing a developer
  • You can define other methods outside of the handler() method and use them when necessary
# Define all texts before the code so that it can be easily edited by the Chatbot manager.
# Access in handler as e.g. texts['errors']['order_not_found']
texts = {
    'success': {
        'order_processed': "Text to display in bot for order {order_id} processed. **Formatting** can be applied. 🥳",
        'order_lost': "Text to display in bot for order seems to be lost. **Formatting** can be applied."
    },
    'errors': {
        'order_not_found': "🥺 Text to display in bot for order not found. **Formatting** can be applied.",
        'could_not_obtain_token': "Can have different error texts for different cases.",
        'generic_error': "Text to display in bot for generic error. **Formatting** can be applied."
    }
}


import logging
import requests


my_endpoint = 'my.api.com/v2/order/{order_id}/'

# Exceptions can be specified to display more detailed texts for user where relevant
class LoginException(Exception):
    pass


# For cleaner or repetitive code functions outside handler can be defined and called in handler
def perform_some_complicated_logic(var1, var2, var3):
    # logic here
    return 'something or nothing'


# Main function - must be defined like this. Handler must always return a result (text displayed in chatbot)
def handler(event, context):

    # 'order_id' and 'post_code' matches Field Keys for customer input fields in WebApp
    order_id = event['order_id']
    post_code = event['post_code']

    url = my_endpoint.format(order_id=order_id)
    try:
        # Call function outside handler
        perform_some_complicated_logic(1, 2, 3)
    
        try:
            ...
            login_token = requests.post(token_url, data=payload)
    
        except requests.HTTPError:
            raise LoginException
    
        return texts['success']['order_processed'].format(order_id=order_id)

    except LoginException:
        return texts['errors']['could_not_obtain_token']

    except Exception:
        return texts['errors']['generic_error']

// Coming soon!

Text formatting

The text returned by the routine visible to the customer in the Widget can be formatted the same way as throughout the Web App:

  • **text** for bold
  • *text* for italic
  • [link_text](url) for a link
  • text\ntext for linebreak
  • Insert emojis 🥳 with Emoji keyboard
  • * el1``* el2 for a list

Interaction with form fields

Interaction with input fields from Widget

Form fields that will appear in the Widget and collect information from the customer are set up and managed in the respective routine component.

Placeholder text, Field type (and respective validation) and Required? are relevant to display and apply user input validation in the Widget.

The respective user input in the routine code can be accessed as event['field_key'] as defined in Field Key.

Input fields
def handler(event, context):
   order_id = event['order_id']
   user_email = event['email']
   key = event['any_field_key_defined_in_input_fields']
   return user_email

// Coming soon!

Optional fields

Even though typically all fields in the routine collected from the customer are mandatory there can be use cases where it makes sense to leave the field optional meaning user can leave it empty.

To avoid KeyError for optional fields you might want to use Python Dictionary get() method.

def handler(event, context):
   # default value - None
   zip = event.get('zip')
   # define default value
   zip_2 = event.get('zip', '12345')
   return "I ❤️ Solvemate!"

// Coming soon!

Recommendations

  • Always apply RegEx for user input fields where possible. It will help to not get unexpected input that the routine can’t handle correctly
  • You might want to clean up and standardize user input once it’s received in the routine, e.g. make lowercase or uppercase, capitalize, remove whitespaces etc.
def handler(event, context):
   order_id = event['order_id'].upper()
   user_email = event['email'].lower()
   key = event['any_field_key_defined_in_input_fields'].strip()
   return user_email

// Coming soon!

Interaction with Secrets in Solvemate Web App

Any sensitive access information e.g. API tokens or login data for authentication with a third-party system can be stored securely in the Solvemate Web App Secrets section of the respective routine component. This prevents this information from being visible in plain text in code. After storing a secret, it cannot be read by any user.

Secrets

To access and use securely stored values in the routine code they need to be decoded.

import os
import boto3

from base64 import b64decode

# os.environ['auth_token'] matches the Secret Key in Solvemate Web App
auth_token_cipher = b64decode(os.environ['auth_token'])
auth_token = boto3.client('kms').decrypt(CiphertextBlob=auth_token_cipher)['Plaintext'].decode('utf-8')


def handler(event, context):
   ...
   headers = {'Authorization': 'Bearer %s' % auth_token,
               'Content-Type': 'application/json'}
   ...
   return "Bots are cool! 🤖"

// Coming soon!